tksbrokerapi.TKSBrokerAPI
TKSBrokerAPI is the trading platform for automation and simplifying the implementation of trading scenarios,
as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways:
from the console, it has a rich keys and commands, or you can use it as Python module with python import.
TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems.
- Open account for trading: http://tinkoff.ru/sl/AaX1Et1omnH
- TKSBrokerAPI module documentation: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html
- See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples
- Used constants are in the TKSEnums module: https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html
- About Tinkoff Invest API: https://tinkoff.github.io/investAPI/
- Tinkoff Invest API documentation: https://tinkoff.github.io/investAPI/swagger-ui/
1# -*- coding: utf-8 -*- 2# Author: Timur Gilmullin 3 4""" 5**TKSBrokerAPI** is the trading platform for automation and simplifying the implementation of trading scenarios, 6as well as working with Tinkoff Invest API server via the REST protocol. The TKSBrokerAPI platform may be used in two ways: 7from the console, it has a rich keys and commands, or you can use it as Python module with `python import`. 8 9TKSBrokerAPI allows you to automate routine trading operations and implement your trading scenarios, or just receive 10the necessary information from the broker. It is easy enough to integrate into various CI/CD automation systems. 11 12- **Open account for trading:** http://tinkoff.ru/sl/AaX1Et1omnH 13- **TKSBrokerAPI module documentation:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSBrokerAPI.html 14- **See examples:** https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md#Usage-examples 15- **Used constants are in the TKSEnums module:** https://tim55667757.github.io/TKSBrokerAPI/docs/tksbrokerapi/TKSEnums.html 16- **About Tinkoff Invest API:** https://tinkoff.github.io/investAPI/ 17- **Tinkoff Invest API documentation:** https://tinkoff.github.io/investAPI/swagger-ui/ 18""" 19 20# Copyright (c) 2022 Gilmillin Timur Mansurovich 21# 22# Licensed under the Apache License, Version 2.0 (the "License"); 23# you may not use this file except in compliance with the License. 24# You may obtain a copy of the License at 25# 26# http://www.apache.org/licenses/LICENSE-2.0 27# 28# Unless required by applicable law or agreed to in writing, software 29# distributed under the License is distributed on an "AS IS" BASIS, 30# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31# See the License for the specific language governing permissions and 32# limitations under the License. 33 34 35import sys 36import os 37from argparse import ArgumentParser 38from importlib.metadata import version 39 40from datetime import datetime, timedelta 41from dateutil.tz import tzlocal, tzutc 42from time import sleep 43 44import re 45import json 46import requests 47import traceback as tb 48from typing import Union 49 50from multiprocessing import cpu_count 51from multiprocessing.pool import ThreadPool 52import pandas as pd 53 54from TKSEnums import * # A lot of constants from enums sections: https://tinkoff.github.io/investAPI/swagger-ui/ 55 56from pricegenerator.PriceGenerator import PriceGenerator, uLogger # This module has a lot of instruments to work with candles data. See docs here: https://github.com/Tim55667757/PriceGenerator 57from pricegenerator.UniLogger import DisableLogger as PGDisLog # Method for disable log from PriceGenerator 58 59import UniLogger as uLog # Logger for TKSBrokerAPI 60 61 62# --- Common technical parameters: 63 64PGDisLog(uLogger.handlers[0]) # Disable 3-rd party logging from PriceGenerator 65uLogger = uLog.UniLogger # init logger for TKSBrokerAPI 66uLogger.level = 10 # debug level by default for TKSBrokerAPI module 67uLogger.handlers[0].level = 20 # info level by default for STDOUT of TKSBrokerAPI module 68 69__version__ = "1.5" # The "major.minor" version setup here, but build number define at the build-server only 70 71CPU_COUNT = cpu_count() # host's real CPU count 72CPU_USAGES = CPU_COUNT - 1 if CPU_COUNT > 1 else 1 # how many CPUs will be used for parallel calculations 73 74# --- Main constants: 75 76NANO = 0.000000001 # SI-constant nano = 10^-9 77 78 79def NanoToFloat(units: str, nano: int) -> float: 80 """ 81 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 82 83 `NanoToFloat(units="2", nano=500000000) -> 2.5` 84 85 `NanoToFloat(units="0", nano=50000000) -> 0.05` 86 87 :param units: integer string or integer parameter that represents the integer part of number 88 :param nano: integer string or integer parameter that represents the fractional part of number 89 :return: float view of number 90 """ 91 return int(units) + int(nano) * NANO 92 93 94def FloatToNano(number: float) -> dict: 95 """ 96 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 97 98 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 99 100 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 101 102 :param number: float number 103 :return: nano-type view of number: `{"units": "string", "nano": integer}` 104 """ 105 splitByPoint = str(number).split(".") 106 frac = 0 107 108 if len(splitByPoint) > 1: 109 if len(splitByPoint[1]) <= 9: 110 frac = int("{}{}".format( 111 int(splitByPoint[1]), 112 "0" * (9 - len(splitByPoint[1])), 113 )) 114 115 if (number < 0) and (frac > 0): 116 frac = -frac 117 118 return {"units": str(int(number)), "nano": frac} 119 120 121def GetDatesAsString(start: str = None, end: str = None) -> tuple: 122 """ 123 Create tuple of date and time strings with timezone parsed from user-friendly date. 124 125 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 126 127 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 128 An error exception will occur if input date has incorrect format. 129 130 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 131 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 132 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 133 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 134 135 Also, you can use keywords for start if `end=None`: 136 `today` (from 00:00:00 to the end of current day), 137 `yesterday` (-1 day from 00:00:00 to 23:59:59), 138 `week` (-7 day from 00:00:00 to the end of current day), 139 `month` (-30 day from 00:00:00 to the end of current day), 140 `year` (-365 day from 00:00:00 to the end of current day), 141 142 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 143 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 144 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 145 """ 146 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 147 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 148 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 149 150 # time between start and the end of the current day: 151 if start is None or start.lower() == "today": 152 pass 153 154 # from start of the last day to the end of the last day: 155 elif start.lower() == "yesterday": 156 s -= timedelta(days=1) 157 e -= timedelta(days=1) 158 159 # week (-7 day from 00:00:00 to the end of the current day): 160 elif start.lower() == "week": 161 s -= timedelta(days=6) # +1 current day already taken into account 162 163 # month (-30 day from 00:00:00 to the end of current day): 164 elif start.lower() == "month": 165 s -= timedelta(days=29) # +1 current day already taken into account 166 167 # year (-365 day from 00:00:00 to the end of current day): 168 elif start.lower() == "year": 169 s -= timedelta(days=364) # +1 current day already taken into account 170 171 # -N days ago to the end of current day: 172 elif start.startswith('-') and start[1:].isdigit(): 173 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 174 175 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 176 else: 177 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 178 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 179 180 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 181 s = s.strftime(TKS_DATE_TIME_FORMAT) 182 e = e.strftime(TKS_DATE_TIME_FORMAT) 183 184 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 185 186 return s, e 187 188 189class TinkoffBrokerServer: 190 """ 191 This class implements methods to work with Tinkoff broker server. 192 193 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 194 195 About `token`: https://tinkoff.github.io/investAPI/token/ 196 """ 197 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 198 """ 199 Main class init. 200 201 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 202 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 203 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 204 :param useCache: use default cache file with raw data to use instead of `iList`. 205 True by default. Cache is auto-update if new day has come. 206 If you don't want to use cache and always updates raw data then set `useCache=False`. 207 :param defaultCache: path to default cache file. `dump.json` by default. 208 """ 209 if token is None or not token: 210 try: 211 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 212 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 213 214 except KeyError: 215 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 216 raise Exception("Token required") 217 218 else: 219 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 220 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 221 222 if accountId is None or not accountId: 223 try: 224 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 225 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 226 227 except KeyError: 228 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 229 230 else: 231 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 232 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 233 234 self.version = __version__ # duplicate here used TKSBrokerAPI main version 235 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 236 237 Latest version: https://pypi.org/project/tksbrokerapi/ 238 """ 239 240 self.aliases = TKS_TICKER_ALIASES 241 """Some aliases instead official tickers. 242 243 See also: `TKSEnums.TKS_TICKER_ALIASES` 244 """ 245 246 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 247 248 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 249 250 self.ticker = "" 251 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 252 253 See also: `SearchByTicker()`, `SearchInstruments()`. 254 """ 255 256 self.figi = "" 257 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 258 259 See also: `SearchByFIGI()`, `SearchInstruments()`. 260 """ 261 262 self.depth = 1 263 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 264 265 See also: `GetCurrentPrices()`. 266 """ 267 268 self.server = r"https://invest-public-api.tinkoff.ru/rest" 269 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 270 271 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 272 """ 273 274 uLogger.debug("Broker API server: {}".format(self.server)) 275 276 self.timeout = 15 277 """Server operations timeout in seconds. Default: `15`. 278 279 See also: `SendAPIRequest()`. 280 """ 281 282 self.headers = { 283 "Content-Type": "application/json", 284 "accept": "application/json", 285 "Authorization": "Bearer {}".format(self.token), 286 "x-app-name": "Tim55667757.TKSBrokerAPI", 287 } 288 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 289 290 See also: `SendAPIRequest()`. 291 """ 292 293 self.body = None 294 """Request body which send to broker server. Default: `None`. 295 296 See also: `SendAPIRequest()`. 297 """ 298 299 self.historyFile = None 300 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 301 302 See also: `History()`. 303 """ 304 305 self.htmlHistoryFile = "index.html" 306 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 307 308 See also: `ShowHistoryChart()`. 309 """ 310 311 self.instrumentsFile = "instruments.md" 312 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 313 314 See also: `ShowInstrumentsInfo()`. 315 """ 316 317 self.searchResultsFile = "search-results.md" 318 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 319 320 See also: `SearchInstruments()`. 321 """ 322 323 self.pricesFile = "prices.md" 324 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 325 326 See also: `GetListOfPrices()`. 327 """ 328 329 self.infoFile = "info.md" 330 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 331 332 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 333 """ 334 335 self.bondsXLSXFile = "ext-bonds.xlsx" 336 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 337 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 338 339 See also: `ExtendBondsData()`. 340 """ 341 342 self.calendarFile = "calendar.md" 343 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 344 345 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 346 347 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 348 """ 349 350 self.overviewFile = "overview.md" 351 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 352 353 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 354 """ 355 356 self.overviewDigestFile = "overview-digest.md" 357 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 358 359 See also: `Overview()` with parameter `details="digest"`. 360 """ 361 362 self.overviewPositionsFile = "overview-positions.md" 363 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 364 365 See also: `Overview()` with parameter `details="positions"`. 366 """ 367 368 self.overviewOrdersFile = "overview-orders.md" 369 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 370 371 See also: `Overview()` with parameter `details="orders"`. 372 """ 373 374 self.overviewAnalyticsFile = "overview-analytics.md" 375 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 376 377 See also: `Overview()` with parameter `details="analytics"`. 378 """ 379 380 self.reportFile = "deals.md" 381 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 382 383 See also: `Deals()`. 384 """ 385 386 self.withdrawalLimitsFile = "limits.md" 387 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 388 389 See also: `OverviewLimits()` and `RequestLimits()`. 390 """ 391 392 self.userInfoFile = "user-info.md" 393 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 394 395 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 396 """ 397 398 self.userAccountsFile = "accounts.md" 399 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 400 401 See also: `OverviewAccounts()`, `RequestAccounts()`. 402 """ 403 404 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 405 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 406 407 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 408 409 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 410 """ 411 412 self.iList = None # init iList for raw instruments data 413 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 414 415 See also: `Listing()`, `DumpInstruments()`. 416 """ 417 418 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 419 if useCache: 420 if os.path.exists(self.iListDumpFile): 421 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 422 curTime = datetime.now(tzutc()) 423 424 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 425 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 426 427 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 428 429 else: 430 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 431 432 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 433 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 434 435 else: 436 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 437 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 438 439 else: 440 self.iList = self.Listing() # request new raw instruments data from broker server 441 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 442 443 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 444 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 445 446 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 447 """ 448 449 @staticmethod 450 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 451 """ 452 Parse JSON from response string. 453 454 :param rawData: this is a string with JSON-formatted text. 455 :param debug: if `True` then print more debug information. 456 :return: JSON (dictionary), parsed from server response string. 457 """ 458 if debug: 459 uLogger.debug("Raw text body:") 460 uLogger.debug(rawData) 461 462 responseJSON = json.loads(rawData) if rawData else {} 463 464 if debug: 465 uLogger.debug("JSON formatted:") 466 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 467 uLogger.debug(jsonLine) 468 469 return responseJSON 470 471 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 472 """ 473 Send GET or POST request to broker server and receive JSON object. 474 475 self.header: must be defining with dictionary of headers. 476 self.body: if define then used as request body. None by default. 477 self.timeout: global request timeout, 15 seconds by default. 478 :param url: url with REST request. 479 :param reqType: send "GET" or "POST" request. "GET" by default. 480 :param retry: how many times retry after first request if an 5xx server errors occurred. 481 :param pause: sleep time in seconds between retries. 482 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 483 :return: response JSON (dictionary) from broker. 484 """ 485 if reqType not in ("GET", "POST"): 486 uLogger.error("You can define request type: 'GET' or 'POST'!") 487 raise Exception("Incorrect value") 488 489 if debug: 490 uLogger.debug("Request parameters:") 491 uLogger.debug(" - REST API URL: {}".format(url)) 492 uLogger.debug(" - request type: {}".format(reqType)) 493 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 494 uLogger.debug(" - body: {}".format(self.body)) 495 496 # fast hack to avoid all operations with some tickers/FIGI 497 responseJSON = {} 498 oK = True 499 for item in self.exclude: 500 if item in url: 501 if debug: 502 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 503 504 oK = False 505 break 506 507 if oK: 508 counter = 0 509 response = None 510 errMsg = "" 511 512 while not response and counter <= retry: 513 if reqType == "GET": 514 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 515 516 if reqType == "POST": 517 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 518 519 if debug: 520 uLogger.debug("Response:") 521 uLogger.debug(" - status code: {}".format(response.status_code)) 522 uLogger.debug(" - reason: {}".format(response.reason)) 523 uLogger.debug(" - body length: {}".format(len(response.text))) 524 uLogger.debug(" - headers: {}".format(response.headers)) 525 526 # Server returns some headers: 527 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 528 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 529 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 530 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 531 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 532 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 533 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 534 sleep(rateLimitWait) 535 536 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 537 if 400 <= response.status_code < 500: 538 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 539 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 540 counter = retry + 1 541 542 if 500 <= response.status_code < 600: 543 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 544 uLogger.debug(" - not oK, {}".format(errMsg)) 545 counter += 1 546 547 if counter <= retry: 548 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 549 sleep(pause) 550 551 responseJSON = self._ParseJSON(response.text) 552 553 if errMsg: 554 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 555 uLogger.error(" - not oK, {}".format(errMsg)) 556 557 return responseJSON 558 559 def _IUpdater(self, iType: str) -> tuple: 560 """ 561 Request instrument by type from server. See available API methods for instruments: 562 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 563 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 564 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 565 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 566 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 567 568 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 569 :return: tuple with iType name and list of available instruments of current type for defined user token. 570 """ 571 result = [] 572 573 if iType in TKS_INSTRUMENTS: 574 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 575 576 # all instruments have the same body in API v2 requests: 577 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 578 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 579 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 580 581 return iType, result 582 583 def _IWrapper(self, kwargs): 584 """ 585 Wrapper runs instrument's update method `_IUpdater()`. 586 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 587 """ 588 return self._IUpdater(**kwargs) 589 590 def Listing(self) -> dict: 591 """ 592 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 593 594 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 595 """ 596 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 597 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 598 599 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 600 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 601 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 602 603 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 604 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 605 poolUpdater.close() 606 607 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 608 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 609 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 610 611 # calculate minimum price increment (step) for all instruments and set up instrument's type: 612 for iType in iList.keys(): 613 for ticker in iList[iType]: 614 iList[iType][ticker]["type"] = iType 615 616 if "minPriceIncrement" in iList[iType][ticker].keys(): 617 iList[iType][ticker]["step"] = NanoToFloat( 618 iList[iType][ticker]["minPriceIncrement"]["units"], 619 iList[iType][ticker]["minPriceIncrement"]["nano"], 620 ) 621 622 else: 623 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 624 625 return iList 626 627 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 628 """ 629 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 630 631 See also: `DumpInstruments()`, `Listing()`. 632 633 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 634 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 635 """ 636 if self.iListDumpFile is None or not self.iListDumpFile: 637 uLogger.error("Output name of dump file must be defined!") 638 raise Exception("Filename required") 639 640 if not self.iList or forceUpdate: 641 self.iList = self.Listing() 642 643 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 644 645 # Save as XLSX with separated sheets for every type of instruments: 646 with pd.ExcelWriter( 647 path=xlsxDumpFile, 648 date_format=TKS_DATE_FORMAT, 649 datetime_format=TKS_DATE_TIME_FORMAT, 650 mode="w", 651 ) as writer: 652 for iType in TKS_INSTRUMENTS: 653 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 654 df = df[sorted(df)] # sorted by column names 655 df = df.applymap( 656 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 657 na_action="ignore", 658 ) # converting numbers from nano-type to float in every cell 659 df.to_excel( 660 writer, 661 sheet_name=iType, 662 encoding="UTF-8", 663 freeze_panes=(1, 1), 664 ) # saving as XLSX-file with freeze first row and column as headers 665 666 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 667 668 def DumpInstruments(self, forceUpdate: bool = True) -> str: 669 """ 670 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 671 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 672 673 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 674 675 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 676 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 677 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 678 """ 679 if self.iListDumpFile is None or not self.iListDumpFile: 680 uLogger.error("Output name of dump file must be defined!") 681 raise Exception("Filename required") 682 683 if not self.iList or forceUpdate: 684 self.iList = self.Listing() 685 686 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 687 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 688 fH.write(jsonDump) 689 690 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 691 692 return jsonDump 693 694 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 695 """ 696 Show information about one instrument defined by json data and prints it in Markdown format. 697 698 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 699 700 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 701 :param show: if `True` then also printing information about instrument and its current price. 702 :return: multilines text in Markdown format with information about one instrument. 703 """ 704 splitLine = "| | |\n" 705 infoText = "" 706 707 if iJSON is not None and iJSON and isinstance(iJSON, dict): 708 info = [ 709 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 710 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 711 "| Parameters | Values |\n", 712 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 713 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 714 "| Full name: | {:<54} |\n".format(iJSON["name"]), 715 ] 716 717 if "sector" in iJSON.keys() and iJSON["sector"]: 718 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 719 720 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 721 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 722 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 723 ))) 724 725 info.extend([ 726 splitLine, 727 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 728 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 729 ]) 730 731 if "isin" in iJSON.keys() and iJSON["isin"]: 732 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 733 734 if "classCode" in iJSON.keys(): 735 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 736 737 info.extend([ 738 splitLine, 739 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 740 splitLine, 741 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 742 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 743 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 744 ]) 745 746 if iJSON["figi"]: 747 self.figi = iJSON["figi"] 748 iJSON = iJSON | self.RequestTradingStatus() 749 750 info.extend([ 751 splitLine, 752 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 753 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 754 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 755 ]) 756 757 info.append(splitLine) 758 759 if "type" in iJSON.keys() and iJSON["type"]: 760 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 761 762 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 763 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 764 765 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 766 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 767 768 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 769 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 770 771 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 772 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 773 774 if "focusType" in iJSON.keys() and iJSON["focusType"]: 775 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 776 777 if "assetType" in iJSON.keys() and iJSON["assetType"]: 778 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 779 780 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 781 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 782 783 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 784 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 785 786 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 787 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 788 789 if "currency" in iJSON.keys(): 790 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 791 792 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 793 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 794 795 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 796 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 797 798 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 799 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 800 801 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 802 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 803 804 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 805 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 806 807 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 808 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 809 810 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 811 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 812 813 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 814 info.append("| Perpetual bond: | Yes |\n") 815 816 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 817 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 818 819 iExt = None 820 if iJSON["type"] == "Bonds": 821 info.extend([ 822 splitLine, 823 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 824 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 825 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 826 iJSON["nominal"]["currency"], 827 )), 828 ]) 829 830 if "floatingCouponFlag" in iJSON.keys(): 831 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 832 833 if "amortizationFlag" in iJSON.keys(): 834 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 835 836 info.append(splitLine) 837 838 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 839 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 840 841 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 842 843 info.extend([ 844 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 845 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 846 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 847 ]) 848 849 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 850 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 851 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 852 iJSON["aciValue"]["currency"] 853 ))) 854 855 if "currentPrice" in iJSON.keys(): 856 info.append(splitLine) 857 858 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 859 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 860 861 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 862 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 863 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 864 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 865 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 866 867 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 868 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 869 870 info.extend([ 871 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 872 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 873 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 874 )), 875 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 876 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 877 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 878 )), 879 "| Changes between last deal price and last close | {:<54} |\n".format( 880 "{:.2f}%{}".format( 881 iJSON["currentPrice"]["changes"], 882 " ({}{:.2f} {})".format( 883 "+" if bondChangesDelta > 0 else "", 884 bondChangesDelta, 885 aciCurrency 886 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 887 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 888 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 889 currency 890 ), 891 ) 892 ), 893 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 894 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 895 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 896 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 897 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 898 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 899 )), 900 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 901 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 902 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 903 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 904 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 905 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 906 )), 907 ]) 908 909 if "lot" in iJSON.keys(): 910 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 911 912 if "step" in iJSON.keys() and iJSON["step"] != 0: 913 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 914 915 # Add bond payment calendar: 916 if iJSON["type"] == "Bonds": 917 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 918 info.extend(["\n", strCalendar]) 919 920 infoText += "".join(info) 921 922 if show: 923 uLogger.info("{}".format(infoText)) 924 925 else: 926 uLogger.debug("{}".format(infoText)) 927 928 if self.infoFile is not None: 929 with open(self.infoFile, "w", encoding="UTF-8") as fH: 930 fH.write(infoText) 931 932 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 933 934 return infoText 935 936 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 937 """ 938 Search and return raw broker's information about instrument by its ticker. 939 `ticker` must be defined! If debug=True then print all debug messages. 940 941 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 942 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 943 :param debug: if `True` then print all debug console messages. 944 :return: JSON formatted data with information about instrument. 945 """ 946 tickerJSON = {} 947 if debug: 948 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 949 950 if not self.ticker: 951 uLogger.warning("self.ticker variable is not be empty!") 952 953 else: 954 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 955 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 956 raise Exception("Instrument not allowed") 957 958 if not self.iList: 959 self.iList = self.Listing() 960 961 if self.ticker in self.iList["Shares"].keys(): 962 tickerJSON = self.iList["Shares"][self.ticker] 963 if debug: 964 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 965 966 elif self.ticker in self.iList["Currencies"].keys(): 967 tickerJSON = self.iList["Currencies"][self.ticker] 968 if debug: 969 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 970 971 elif self.ticker in self.iList["Bonds"].keys(): 972 tickerJSON = self.iList["Bonds"][self.ticker] 973 if debug: 974 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 975 976 elif self.ticker in self.iList["Etfs"].keys(): 977 tickerJSON = self.iList["Etfs"][self.ticker] 978 if debug: 979 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 980 981 elif self.ticker in self.iList["Futures"].keys(): 982 tickerJSON = self.iList["Futures"][self.ticker] 983 if debug: 984 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 985 986 if tickerJSON: 987 self.figi = tickerJSON["figi"] 988 989 if requestPrice: 990 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 991 992 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 993 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 994 995 else: 996 tickerJSON["currentPrice"]["changes"] = 0 997 998 if show: 999 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1000 1001 else: 1002 if show: 1003 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1004 1005 return tickerJSON 1006 1007 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1008 """ 1009 Search and return raw broker's information about instrument by its FIGI. 1010 `figi` must be defined! If debug=True then print all debug messages. 1011 1012 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1013 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1014 :param debug: if `True` then print all debug console messages. 1015 :return: JSON formatted data with information about instrument. 1016 """ 1017 figiJSON = {} 1018 if debug: 1019 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1020 1021 if not self.figi: 1022 uLogger.warning("self.figi variable is not be empty!") 1023 1024 else: 1025 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1026 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1027 raise Exception("Instrument not allowed") 1028 1029 if not self.iList: 1030 self.iList = self.Listing() 1031 1032 for item in self.iList["Shares"].keys(): 1033 if self.figi == self.iList["Shares"][item]["figi"]: 1034 figiJSON = self.iList["Shares"][item] 1035 1036 if debug: 1037 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1038 1039 break 1040 1041 if not figiJSON: 1042 for item in self.iList["Currencies"].keys(): 1043 if self.figi == self.iList["Currencies"][item]["figi"]: 1044 figiJSON = self.iList["Currencies"][item] 1045 1046 if debug: 1047 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1048 1049 break 1050 1051 if not figiJSON: 1052 for item in self.iList["Bonds"].keys(): 1053 if self.figi == self.iList["Bonds"][item]["figi"]: 1054 figiJSON = self.iList["Bonds"][item] 1055 1056 if debug: 1057 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1058 1059 break 1060 1061 if not figiJSON: 1062 for item in self.iList["Etfs"].keys(): 1063 if self.figi == self.iList["Etfs"][item]["figi"]: 1064 figiJSON = self.iList["Etfs"][item] 1065 1066 if debug: 1067 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1068 1069 break 1070 1071 if not figiJSON: 1072 for item in self.iList["Futures"].keys(): 1073 if self.figi == self.iList["Futures"][item]["figi"]: 1074 figiJSON = self.iList["Futures"][item] 1075 1076 if debug: 1077 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1078 1079 break 1080 1081 if figiJSON: 1082 self.figi = figiJSON["figi"] 1083 self.ticker = figiJSON["ticker"] 1084 1085 if requestPrice: 1086 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1087 1088 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1089 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1090 1091 else: 1092 figiJSON["currentPrice"]["changes"] = 0 1093 1094 if show: 1095 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1096 1097 else: 1098 if show: 1099 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1100 1101 return figiJSON 1102 1103 def GetCurrentPrices(self, show: bool = True) -> dict: 1104 """ 1105 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1106 `{"buy": [{"price": 1243.8, "quantity": 193}, 1107 {"price": 1244.0, "quantity": 168}, 1108 {"price": 1244.8, "quantity": 5}, 1109 {"price": 1245.0, "quantity": 61}, 1110 {"price": 1245.4, "quantity": 60}], 1111 "sell": [{"price": 1243.6, "quantity": 8}, 1112 {"price": 1242.6, "quantity": 10}, 1113 {"price": 1242.4, "quantity": 18}, 1114 {"price": 1242.2, "quantity": 50}, 1115 {"price": 1242.0, "quantity": 113}], 1116 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1117 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1118 - sell: list of dicts with Buyers prices, 1119 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1120 - quantity: volume value by current price in lots, 1121 - limitUp: current trade session limit price, maximum, 1122 - limitDown: current trade session limit price, minimum, 1123 - lastPrice: last deal price of the instrument, 1124 - closePrice: previous trade session close price of the instrument. 1125 1126 See also: `SearchByTicker()` and `SearchByFIGI()`. 1127 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1128 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1129 1130 :param show: if `True` then print DOM to log and console. 1131 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1132 If an error occurred then returns an empty record: 1133 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1134 """ 1135 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1136 1137 if self.depth < 1: 1138 uLogger.error("Depth of Market (DOM) must be >=1!") 1139 raise Exception("Incorrect value") 1140 1141 if not (self.ticker or self.figi): 1142 uLogger.error("self.ticker or self.figi variables must be defined!") 1143 raise Exception("Ticker or FIGI required") 1144 1145 if self.ticker and not self.figi: 1146 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1147 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1148 1149 if not self.ticker and self.figi: 1150 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1151 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1152 1153 if not self.figi: 1154 uLogger.error("FIGI is not defined!") 1155 raise Exception("Ticker or FIGI required") 1156 1157 else: 1158 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1159 1160 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1161 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1162 self.body = str({"figi": self.figi, "depth": self.depth}) 1163 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1164 1165 if pricesResponse: 1166 # list of dicts with sellers orders: 1167 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1168 1169 # list of dicts with buyers orders: 1170 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1171 1172 # max price of instrument at this time: 1173 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1174 1175 # min price of instrument at this time: 1176 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1177 1178 # last price of deal with instrument: 1179 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1180 1181 # last close price of instrument: 1182 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1183 1184 else: 1185 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1186 uLogger.debug("Server response: {}".format(pricesResponse)) 1187 1188 if show: 1189 if prices["buy"] or prices["sell"]: 1190 info = [ 1191 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1192 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1193 self.ticker, 1194 self.figi, 1195 self.depth, 1196 ), 1197 "-" * 60, "\n", 1198 " Orders of Buyers | Orders of Sellers\n", 1199 "-" * 60, "\n", 1200 " Sell prices (volumes) | Buy prices (volumes)\n", 1201 "-" * 60, "\n", 1202 ] 1203 1204 if not prices["buy"]: 1205 info.append(" | No orders!\n") 1206 sumBuy = 0 1207 1208 else: 1209 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1210 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1211 for item in maxMinSorted: 1212 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1213 1214 if not prices["sell"]: 1215 info.append("No orders! |\n") 1216 sumSell = 0 1217 1218 else: 1219 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1220 for item in prices["sell"]: 1221 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1222 1223 info.extend([ 1224 "-" * 60, "\n", 1225 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1226 "-" * 60, "\n", 1227 ]) 1228 1229 infoText = "".join(info) 1230 1231 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1232 1233 else: 1234 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1235 1236 return prices 1237 1238 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1239 """ 1240 This method get and show information about all available broker instruments for current user account. 1241 If `instrumentsFile` string is not empty then also save information to this file. 1242 1243 :param show: if `True` then print results to console, if `False` - print only to file. 1244 :return: multi-lines string with all available broker instruments 1245 """ 1246 if not self.iList: 1247 self.iList = self.Listing() 1248 1249 info = [ 1250 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1251 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1252 ] 1253 1254 # add instruments count by type: 1255 for iType in self.iList.keys(): 1256 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1257 1258 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1259 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1260 1261 # generating info tables with all instruments by type: 1262 for iType in self.iList.keys(): 1263 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1264 1265 for instrument in self.iList[iType].keys(): 1266 iName = self.iList[iType][instrument]["name"] # instrument's name 1267 if len(iName) > 57: 1268 iName = "{}...".format(iName[:54]) # right trim for a long string 1269 1270 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1271 self.iList[iType][instrument]["ticker"], 1272 iName, 1273 self.iList[iType][instrument]["figi"], 1274 self.iList[iType][instrument]["currency"], 1275 self.iList[iType][instrument]["lot"], 1276 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1277 )) 1278 1279 infoText = "".join(info) 1280 1281 if show: 1282 uLogger.info(infoText) 1283 1284 if self.instrumentsFile: 1285 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1286 fH.write(infoText) 1287 1288 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1289 1290 return infoText 1291 1292 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1293 """ 1294 This method search and show information about instruments by part of its ticker, FIGI or name. 1295 If `searchResultsFile` string is not empty then also save information to this file. 1296 1297 :param pattern: string with part of ticker, FIGI or instrument's name. 1298 :param show: if `True` then print results to console, if `False` - return list of result only. 1299 :return: list of dictionaries with all found instruments. 1300 """ 1301 if not self.iList: 1302 self.iList = self.Listing() 1303 1304 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1305 compiledPattern = re.compile(pattern, re.IGNORECASE) 1306 1307 for iType in self.iList: 1308 for instrument in self.iList[iType].values(): 1309 searchResult = compiledPattern.search(" ".join( 1310 [instrument["ticker"], instrument["figi"], instrument["name"]] 1311 )) 1312 1313 if searchResult: 1314 searchResults[iType][instrument["ticker"]] = instrument 1315 1316 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1317 info = [ 1318 "# Search results\n\n", 1319 "* **Search pattern:** [{}]\n".format(pattern), 1320 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1321 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1322 ] 1323 infoShort = info[:] 1324 1325 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1326 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1327 skippedLine = "| ... | ... | ... | ... |\n" 1328 1329 if resultsLen == 0: 1330 info.append("\nNo results\n") 1331 infoShort.append("\nNo results\n") 1332 uLogger.warning("No results. Try changing your search pattern.") 1333 1334 else: 1335 for iType in searchResults: 1336 iTypeValuesCount = len(searchResults[iType].values()) 1337 if iTypeValuesCount > 0: 1338 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1339 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 1341 for instrument in searchResults[iType].values(): 1342 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1343 instrument["type"], 1344 instrument["ticker"], 1345 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1346 instrument["figi"], 1347 )) 1348 1349 if iTypeValuesCount <= 5: 1350 infoShort.extend(info[-iTypeValuesCount:]) 1351 1352 else: 1353 infoShort.extend(info[-5:]) 1354 infoShort.append(skippedLine) 1355 1356 infoText = "".join(info) 1357 infoTextShort = "".join(infoShort) 1358 1359 if show: 1360 uLogger.info(infoTextShort) 1361 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1362 1363 if self.searchResultsFile: 1364 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1365 fH.write(infoText) 1366 1367 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1368 1369 return searchResults 1370 1371 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1372 """ 1373 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1374 1375 :param instruments: list of strings with tickers or FIGIs. 1376 :return: list with unique instrument FIGIs only. 1377 """ 1378 requestedInstruments = [] 1379 for iName in instruments: 1380 if iName not in self.aliases.keys(): 1381 if iName not in requestedInstruments: 1382 requestedInstruments.append(iName) 1383 1384 else: 1385 if iName not in requestedInstruments: 1386 if self.aliases[iName] not in requestedInstruments: 1387 requestedInstruments.append(self.aliases[iName]) 1388 1389 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1390 1391 onlyUniqueFIGIs = [] 1392 for iName in requestedInstruments: 1393 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1394 continue 1395 1396 self.ticker = iName 1397 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1398 1399 if not iData: 1400 self.ticker = "" 1401 self.figi = iName 1402 1403 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1404 1405 if not iData: 1406 self.figi = "" 1407 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1408 1409 if iData and iData["figi"] not in onlyUniqueFIGIs: 1410 onlyUniqueFIGIs.append(iData["figi"]) 1411 1412 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1413 1414 return onlyUniqueFIGIs 1415 1416 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1417 """ 1418 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1419 See limits: https://tinkoff.github.io/investAPI/limits/ 1420 If `pricesFile` string is not empty then also save information to this file. 1421 1422 :param instruments: list of strings with tickers or FIGIs. 1423 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1424 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1425 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1426 """ 1427 if instruments is None or not instruments: 1428 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1429 raise Exception("Ticker or FIGI required") 1430 1431 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1432 1433 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1434 1435 iList = [] # trying to get info and current prices about all unique instruments: 1436 for self.figi in onlyUniqueFIGIs: 1437 iData = self.SearchByFIGI(requestPrice=True) 1438 iList.append(iData) 1439 1440 self.ShowListOfPrices(iList, show) 1441 1442 return iList 1443 1444 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1445 """ 1446 Show table contains current prices of given instruments. 1447 1448 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1449 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1450 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1451 :return: multilines text in Markdown format as a table contains current prices. 1452 """ 1453 infoText = "" 1454 1455 if show or self.pricesFile: 1456 info = [ 1457 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1458 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1459 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1460 ] 1461 1462 for item in iList: 1463 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1464 item["ticker"], 1465 item["figi"], 1466 item["type"], 1467 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1468 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1469 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1470 "{} / {}".format( 1471 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1472 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1473 ), 1474 "{} / {}".format( 1475 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1476 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1477 ), 1478 item["currency"], 1479 )) 1480 1481 infoText = "".join(info) 1482 1483 if show: 1484 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1485 1486 if self.pricesFile: 1487 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1488 fH.write(infoText) 1489 1490 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1491 1492 return infoText 1493 1494 def RequestTradingStatus(self) -> dict: 1495 """ 1496 Requesting trading status for the instrument defined by `figi` variable. 1497 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1498 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1499 1500 :return: dictionary with trading status attributes. Response example: 1501 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1502 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1503 """ 1504 if self.figi is None or not self.figi: 1505 uLogger.error("Variable `figi` must be defined for using this method!") 1506 raise Exception("FIGI required") 1507 1508 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1509 1510 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1511 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1512 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1513 1514 uLogger.debug("Records about current trading status successfully received") 1515 1516 return tradingStatus 1517 1518 def RequestPortfolio(self) -> dict: 1519 """ 1520 Requesting actual user's portfolio for current `accountId`. 1521 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1522 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1523 1524 :return: dictionary with user's portfolio. 1525 """ 1526 if self.accountId is None or not self.accountId: 1527 uLogger.error("Variable `accountId` must be defined for using this method!") 1528 raise Exception("Account ID required") 1529 1530 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1531 1532 self.body = str({"accountId": self.accountId}) 1533 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1534 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1535 1536 uLogger.debug("Records about user's portfolio successfully received") 1537 1538 return rawPortfolio 1539 1540 def RequestPositions(self) -> dict: 1541 """ 1542 Requesting open positions by currencies and instruments for current `accountId`. 1543 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1544 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1545 1546 :return: dictionary with open positions by instruments. 1547 """ 1548 if self.accountId is None or not self.accountId: 1549 uLogger.error("Variable `accountId` must be defined for using this method!") 1550 raise Exception("Account ID required") 1551 1552 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1553 1554 self.body = str({"accountId": self.accountId}) 1555 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1556 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1557 1558 uLogger.debug("Records about current open positions successfully received") 1559 1560 return rawPositions 1561 1562 def RequestPendingOrders(self) -> list: 1563 """ 1564 Requesting current actual pending orders for current `accountId`. 1565 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1566 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1567 1568 :return: list of dictionaries with pending orders. 1569 """ 1570 if self.accountId is None or not self.accountId: 1571 uLogger.error("Variable `accountId` must be defined for using this method!") 1572 raise Exception("Account ID required") 1573 1574 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1575 1576 self.body = str({"accountId": self.accountId}) 1577 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1578 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1579 1580 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1581 1582 return rawOrders 1583 1584 def RequestStopOrders(self) -> list: 1585 """ 1586 Requesting current actual stop orders for current `accountId`. 1587 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1588 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1589 1590 :return: list of dictionaries with stop orders. 1591 """ 1592 if self.accountId is None or not self.accountId: 1593 uLogger.error("Variable `accountId` must be defined for using this method!") 1594 raise Exception("Account ID required") 1595 1596 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1597 1598 self.body = str({"accountId": self.accountId}) 1599 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1600 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1601 1602 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1603 1604 return rawStopOrders 1605 1606 def Overview(self, show: bool = False, details: str = "full") -> dict: 1607 """ 1608 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1609 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1610 are defined then also save information to file. 1611 1612 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1613 many requests about the state of the portfolio, and then, based on the received data, a large number 1614 of calculation and statistics are collected. 1615 1616 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1617 :param details: how detailed should the information be? You should specify one of strings: 1618 `full` - shows full available information about portfolio status (by default), 1619 `positions` - shows only open positions, 1620 `digest` - show a short digest of the portfolio status, 1621 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1622 `orders` - shows only sections of open limits and stop orders. 1623 :return: dictionary with client's raw portfolio and some statistics. 1624 """ 1625 if self.accountId is None or not self.accountId: 1626 uLogger.error("Variable `accountId` must be defined for using this method!") 1627 raise Exception("Account ID required") 1628 1629 view = { 1630 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1631 "headers": {}, # list of dictionaries, response headers without "positions" section 1632 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1633 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1634 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1635 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1636 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1637 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1638 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1639 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1640 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1641 }, 1642 "stat": { # --- some statistics calculated using "raw" sections: 1643 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1644 "availableRUB": 0., # available rubles (without other currencies) 1645 "blockedRUB": 0., # blocked sum in Russian Rouble 1646 "totalChangesRUB": 0., # changes for all open trades in RUB 1647 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1648 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1649 "sharesCostRUB": 0., # costs of all shares in RUB 1650 "bondsCostRUB": 0., # costs of all bonds in RUB 1651 "etfsCostRUB": 0., # costs of all etfs in RUB 1652 "futuresCostRUB": 0., # costs of all futures in RUB 1653 "Currencies": [], # list of dictionaries of all currencies statistics 1654 "Shares": [], # list of dictionaries of all shares statistics 1655 "Bonds": [], # list of dictionaries of all bonds statistics 1656 "Etfs": [], # list of dictionaries of all etfs statistics 1657 "Futures": [], # list of dictionaries of all futures statistics 1658 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1659 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1660 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1661 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1662 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1663 }, 1664 "analytics": { # --- some analytics of portfolio: 1665 "distrByAssets": {}, # portfolio distribution by assets 1666 "distrByCompanies": {}, # portfolio distribution by companies 1667 "distrBySectors": {}, # portfolio distribution by sectors 1668 "distrByCurrencies": {}, # portfolio distribution by currencies 1669 "distrByCountries": {}, # portfolio distribution by countries 1670 } 1671 } 1672 1673 details = details.lower() 1674 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1675 if details not in availableDetails: 1676 details = "full" 1677 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1678 1679 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1680 1681 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1682 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1683 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1684 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1685 1686 # save response headers without "positions" section: 1687 for key in portfolioResponse.keys(): 1688 if key != "positions": 1689 view["raw"]["headers"][key] = portfolioResponse[key] 1690 1691 else: 1692 continue 1693 1694 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1695 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1696 for item in portfolioResponse["positions"]: 1697 if item["instrumentType"] == "currency": 1698 self.figi = item["figi"] 1699 curr = self.SearchByFIGI(requestPrice=False) 1700 1701 # current price of currency in RUB: 1702 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1703 "name": curr["name"], 1704 "currentPrice": NanoToFloat( 1705 item["currentPrice"]["units"], 1706 item["currentPrice"]["nano"] 1707 ), 1708 } 1709 1710 view["raw"]["Currencies"].append(item) 1711 1712 elif item["instrumentType"] == "share": 1713 view["raw"]["Shares"].append(item) 1714 1715 elif item["instrumentType"] == "bond": 1716 view["raw"]["Bonds"].append(item) 1717 1718 elif item["instrumentType"] == "etf": 1719 view["raw"]["Etfs"].append(item) 1720 1721 elif item["instrumentType"] == "futures": 1722 view["raw"]["Futures"].append(item) 1723 1724 else: 1725 continue 1726 1727 # how many volume of currencies (by ISO currency name) are blocked: 1728 for item in view["raw"]["positions"]["blocked"]: 1729 blocked = NanoToFloat(item["units"], item["nano"]) 1730 if blocked > 0: 1731 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1732 1733 # how many volume of instruments (by FIGI) are blocked: 1734 for item in view["raw"]["positions"]["securities"]: 1735 blocked = int(item["blocked"]) 1736 if blocked > 0: 1737 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1738 1739 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1740 1741 if "rub" in allBlocked.keys(): 1742 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1743 1744 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1745 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1746 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1747 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1748 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1749 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1750 view["stat"]["portfolioCostRUB"] = sum([ 1751 view["stat"]["allCurrenciesCostRUB"], 1752 view["stat"]["sharesCostRUB"], 1753 view["stat"]["bondsCostRUB"], 1754 view["stat"]["etfsCostRUB"], 1755 view["stat"]["futuresCostRUB"], 1756 ]) 1757 1758 # --- calculating some portfolio statistics: 1759 byComp = {} # distribution by companies 1760 bySect = {} # distribution by sectors 1761 byCurr = {} # distribution by currencies (include RUB) 1762 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1763 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1764 1765 for item in portfolioResponse["positions"]: 1766 self.figi = item["figi"] 1767 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1768 1769 if instrument: 1770 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1771 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1772 1773 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1774 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1775 1776 else: 1777 blocked = 0 1778 1779 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1780 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1781 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1782 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1783 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1784 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1785 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1786 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1787 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1788 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1789 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1790 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1791 1792 statData = { 1793 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1794 "ticker": instrument["ticker"], # ticker by FIGI 1795 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1796 "volume": volume, # available volume of instrument 1797 "lots": lots, # volume in lots of instrument 1798 "direction": direction, # direction of an instrument's position: short or long 1799 "blocked": blocked, # blocked volume of currency or instrument 1800 "currentPrice": curPrice, # current instrument's price in basic asset 1801 "average": average, # current average position price 1802 "cost": cost, # current cost of all volume of instrument in basic asset 1803 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1804 "costRUB": costRUB, # cost of instrument in ruble 1805 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1806 "profit": profit, # expected profit at current moment 1807 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1808 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1809 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1810 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1811 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1812 "step": instrument["step"], # minimum price increment 1813 } 1814 1815 # adding distribution by unique countries: 1816 if statData["country"] not in byCountry.keys(): 1817 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1818 1819 else: 1820 byCountry[statData["country"]]["cost"] += costRUB 1821 byCountry[statData["country"]]["percent"] += percentCostRUB 1822 1823 if item["instrumentType"] != "currency": 1824 # adding distribution by unique companies: 1825 if statData["name"]: 1826 if statData["name"] not in byComp.keys(): 1827 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1828 1829 else: 1830 byComp[statData["name"]]["cost"] += costRUB 1831 byComp[statData["name"]]["percent"] += percentCostRUB 1832 1833 # adding distribution by unique sectors: 1834 if statData["sector"] not in bySect.keys(): 1835 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1836 1837 else: 1838 bySect[statData["sector"]]["cost"] += costRUB 1839 bySect[statData["sector"]]["percent"] += percentCostRUB 1840 1841 # adding distribution by unique currencies: 1842 if currency not in byCurr.keys(): 1843 byCurr[currency] = { 1844 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1845 "cost": costRUB, 1846 "percent": percentCostRUB 1847 } 1848 1849 else: 1850 byCurr[currency]["cost"] += costRUB 1851 byCurr[currency]["percent"] += percentCostRUB 1852 1853 # saving statistics for every instrument: 1854 if item["instrumentType"] == "currency": 1855 view["stat"]["Currencies"].append(statData) 1856 1857 # update dict with free funds for trading (total - blocked) by currencies 1858 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1859 view["stat"]["funds"][currency] = { 1860 "total": volume, 1861 "totalCostRUB": costRUB, # total volume cost in rubles 1862 "free": volume - blocked, 1863 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1864 } 1865 1866 elif item["instrumentType"] == "share": 1867 view["stat"]["Shares"].append(statData) 1868 1869 elif item["instrumentType"] == "bond": 1870 view["stat"]["Bonds"].append(statData) 1871 1872 elif item["instrumentType"] == "etf": 1873 view["stat"]["Etfs"].append(statData) 1874 1875 elif item["instrumentType"] == "Futures": 1876 view["stat"]["Futures"].append(statData) 1877 1878 else: 1879 continue 1880 1881 # total changes in Russian Ruble: 1882 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1883 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1884 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1885 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1886 view["stat"]["funds"]["rub"] = { 1887 "total": view["stat"]["availableRUB"], 1888 "totalCostRUB": view["stat"]["availableRUB"], 1889 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1890 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 } 1892 1893 # --- pending orders sector data: 1894 uniquePendingOrders = [] 1895 uniquePendingOrdersFIGIs = [] 1896 for item in view["raw"]["orders"]: 1897 if item["figi"] not in uniquePendingOrdersFIGIs: 1898 uniquePendingOrdersFIGIs.append(item["figi"]) 1899 uniquePendingOrders.append(item) 1900 1901 for item in uniquePendingOrders: 1902 self.figi = item["figi"] 1903 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1904 1905 if instrument: 1906 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1907 orderType = TKS_ORDER_TYPES[item["orderType"]] 1908 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1909 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1910 1911 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1912 if item["direction"] == "ORDER_DIRECTION_BUY": 1913 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1914 1915 else: 1916 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1917 1918 # requested price for order execution: 1919 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1920 1921 # necessary changes in percent to reach target from current price: 1922 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1923 1924 view["stat"]["orders"].append({ 1925 "orderID": item["orderId"], # orderId number parameter of current order 1926 "figi": item["figi"], # FIGI identification 1927 "ticker": instrument["ticker"], # ticker name by FIGI 1928 "lotsRequested": item["lotsRequested"], # requested lots value 1929 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1930 "currentPrice": lastPrice, # current instrument's price for defined action 1931 "targetPrice": target, # requested price for order execution in base currency 1932 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1933 "percentChanges": changes, # changes in percent to target from current price 1934 "currency": item["currency"], # instrument's currency name 1935 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1936 "type": orderType, # type of order from TKS_ORDER_TYPES 1937 "status": orderState, # order status from TKS_ORDER_STATES 1938 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1939 }) 1940 1941 # --- stop orders sector data: 1942 uniqueStopOrders = [] 1943 uniqueStopOrdersFIGIs = [] 1944 for item in view["raw"]["stopOrders"]: 1945 if item["figi"] not in uniqueStopOrdersFIGIs: 1946 uniqueStopOrdersFIGIs.append(item["figi"]) 1947 uniqueStopOrders.append(item) 1948 1949 for item in uniqueStopOrders: 1950 self.figi = item["figi"] 1951 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1952 1953 if instrument: 1954 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1955 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1956 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1957 1958 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1959 if "expirationTime" in item.keys(): 1960 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1961 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1962 1963 else: 1964 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1965 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1966 1967 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1968 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1969 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1970 1971 else: 1972 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1973 1974 # requested price when stop-order executed: 1975 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1976 1977 # price for limit-order, set up when stop-order executed: 1978 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1979 1980 # necessary changes in percent to reach target from current price: 1981 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1982 1983 view["stat"]["stopOrders"].append({ 1984 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1985 "figi": item["figi"], # FIGI identification 1986 "ticker": instrument["ticker"], # ticker name by FIGI 1987 "lotsRequested": item["lotsRequested"], # requested lots value 1988 "currentPrice": lastPrice, # current instrument's price for defined action 1989 "targetPrice": target, # requested price for stop-order execution in base currency 1990 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1991 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1992 "percentChanges": changes, # changes in percent to target from current price 1993 "currency": item["currency"], # instrument's currency name 1994 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1995 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1996 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1997 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1998 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 1999 }) 2000 2001 # --- calculating data for analytics section: 2002 # portfolio distribution by assets: 2003 view["analytics"]["distrByAssets"] = { 2004 "Ruble": { 2005 "uniques": 1, 2006 "cost": view["stat"]["availableRUB"], 2007 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2008 }, 2009 "Currencies": { 2010 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2011 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2012 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2013 }, 2014 "Shares": { 2015 "uniques": len(view["stat"]["Shares"]), 2016 "cost": view["stat"]["sharesCostRUB"], 2017 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2018 }, 2019 "Bonds": { 2020 "uniques": len(view["stat"]["Bonds"]), 2021 "cost": view["stat"]["bondsCostRUB"], 2022 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2023 }, 2024 "Etfs": { 2025 "uniques": len(view["stat"]["Etfs"]), 2026 "cost": view["stat"]["etfsCostRUB"], 2027 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2028 }, 2029 "Futures": { 2030 "uniques": len(view["stat"]["Futures"]), 2031 "cost": view["stat"]["futuresCostRUB"], 2032 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2033 }, 2034 } 2035 2036 # portfolio distribution by companies: 2037 view["analytics"]["distrByCompanies"]["All money cash"] = { 2038 "ticker": "", 2039 "cost": view["stat"]["allCurrenciesCostRUB"], 2040 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2041 } 2042 view["analytics"]["distrByCompanies"].update(byComp) 2043 2044 # portfolio distribution by sectors: 2045 view["analytics"]["distrBySectors"]["All money cash"] = { 2046 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2047 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2048 } 2049 view["analytics"]["distrBySectors"].update(bySect) 2050 2051 # portfolio distribution by currencies: 2052 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2053 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2054 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2055 2056 view["analytics"]["distrByCurrencies"].update(byCurr) 2057 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2058 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2059 2060 # portfolio distribution by countries: 2061 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2062 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2063 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2064 2065 view["analytics"]["distrByCountries"].update(byCountry) 2066 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2068 2069 # --- Prepare text statistics overview in human-readable: 2070 if show: 2071 # Whatever the value `details`, header not changes: 2072 info = [ 2073 "# Client's portfolio\n\n", 2074 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2075 "* **Account ID:** [{}]\n".format(self.accountId), 2076 ] 2077 2078 if details in ["full", "positions", "digest"]: 2079 info.extend([ 2080 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2081 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2082 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2083 view["stat"]["totalChangesRUB"], 2084 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2085 view["stat"]["totalChangesPercentRUB"], 2086 ), 2087 ]) 2088 2089 if details in ["full", "positions"]: 2090 info.extend([ 2091 "## Open positions\n\n", 2092 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2093 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2094 "| Ruble | {:>31} | | | | | |\n".format( 2095 "{:.2f} ({:.2f}) rub".format( 2096 view["stat"]["availableRUB"], 2097 view["stat"]["blockedRUB"], 2098 ) 2099 ) 2100 ]) 2101 2102 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2103 return [ 2104 "| | | | | | | |\n", 2105 "| {:<27} | | | | | {:>19} | |\n".format( 2106 noTradeStr if noTradeStr else typeStr, 2107 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2108 ), 2109 ] 2110 2111 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2112 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2113 "{} [{}]".format(data["ticker"], data["figi"]), 2114 "{:.2f} ({:.2f}) {}".format( 2115 data["volume"], 2116 data["blocked"], 2117 data["currency"], 2118 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2119 data["volume"], 2120 data["blocked"], 2121 ), 2122 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2123 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2124 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2126 "{}{:.2f} {} ({}{:.2f}%)".format( 2127 "+" if data["profit"] > 0 else "", 2128 data["profit"], data["baseCurrencyName"], 2129 "+" if data["percentProfit"] > 0 else "", 2130 data["percentProfit"], 2131 ), 2132 ) 2133 2134 # --- Show currencies section: 2135 if view["stat"]["Currencies"]: 2136 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2137 for item in view["stat"]["Currencies"]: 2138 info.append(_InfoStr(item, showCurrencyName=True)) 2139 2140 else: 2141 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2142 2143 # --- Show shares section: 2144 if view["stat"]["Shares"]: 2145 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2146 2147 for item in view["stat"]["Shares"]: 2148 info.append(_InfoStr(item)) 2149 2150 else: 2151 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2152 2153 # --- Show bonds section: 2154 if view["stat"]["Bonds"]: 2155 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2156 2157 for item in view["stat"]["Bonds"]: 2158 info.append(_InfoStr(item)) 2159 2160 else: 2161 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2162 2163 # --- Show etfs section: 2164 if view["stat"]["Etfs"]: 2165 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2166 2167 for item in view["stat"]["Etfs"]: 2168 info.append(_InfoStr(item)) 2169 2170 else: 2171 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2172 2173 # --- Show futures section: 2174 if view["stat"]["Futures"]: 2175 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2176 2177 for item in view["stat"]["Futures"]: 2178 info.append(_InfoStr(item)) 2179 2180 else: 2181 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2182 2183 if details in ["full", "orders"]: 2184 # --- Show pending orders section: 2185 if view["stat"]["orders"]: 2186 info.extend([ 2187 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2188 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2189 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2190 ]) 2191 2192 for item in view["stat"]["orders"]: 2193 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2194 "{} [{}]".format(item["ticker"], item["figi"]), 2195 item["orderID"], 2196 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2197 "{} {} ({}{:.2f}%)".format( 2198 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2199 item["baseCurrencyName"], 2200 "+" if item["percentChanges"] > 0 else "", 2201 float(item["percentChanges"]), 2202 ), 2203 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2204 item["action"], 2205 item["type"], 2206 item["date"], 2207 )) 2208 2209 else: 2210 info.append("\n## Total pending limit-orders: 0\n") 2211 2212 # --- Show stop orders section: 2213 if view["stat"]["stopOrders"]: 2214 info.extend([ 2215 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2216 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2217 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2218 ]) 2219 2220 for item in view["stat"]["stopOrders"]: 2221 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2222 "{} [{}]".format(item["ticker"], item["figi"]), 2223 item["orderID"], 2224 item["lotsRequested"], 2225 "{} {} ({}{:.2f}%)".format( 2226 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2227 item["baseCurrencyName"], 2228 "+" if item["percentChanges"] > 0 else "", 2229 float(item["percentChanges"]), 2230 ), 2231 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2232 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2233 item["action"], 2234 item["type"], 2235 item["expType"], 2236 item["createDate"], 2237 item["expDate"], 2238 )) 2239 2240 else: 2241 info.append("\n## Total stop-orders: 0\n") 2242 2243 if details in ["full", "analytics"]: 2244 # -- Show analytics section: 2245 if view["stat"]["portfolioCostRUB"] > 0: 2246 info.extend([ 2247 "\n# Analytics\n" 2248 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2249 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2250 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2251 view["stat"]["totalChangesRUB"], 2252 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2253 view["stat"]["totalChangesPercentRUB"], 2254 ), 2255 "\n## Portfolio distribution by assets\n" 2256 "\n| Type | Uniques | Percent | Current cost |\n", 2257 "|------------|---------|---------|--------------------|\n", 2258 ]) 2259 2260 for key in view["analytics"]["distrByAssets"].keys(): 2261 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2262 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2263 key, 2264 view["analytics"]["distrByAssets"][key]["uniques"], 2265 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2266 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2267 )) 2268 2269 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2270 info.extend([ 2271 "\n## Portfolio distribution by companies\n" 2272 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2273 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2274 ]) 2275 2276 for company in view["analytics"]["distrByCompanies"].keys(): 2277 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2278 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2279 info.append("| {} | {:<7} | {:<18} |\n".format( 2280 "{}{}{}".format( 2281 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2282 company, 2283 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2284 ), 2285 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2286 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2287 )) 2288 2289 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2290 info.extend([ 2291 "\n## Portfolio distribution by sectors\n" 2292 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2293 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2294 ]) 2295 2296 for sector in view["analytics"]["distrBySectors"].keys(): 2297 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2298 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2299 sector, 2300 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2301 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2302 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2303 )) 2304 2305 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2306 info.extend([ 2307 "\n## Portfolio distribution by currencies\n" 2308 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2309 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2310 ]) 2311 2312 for curr in view["analytics"]["distrByCurrencies"].keys(): 2313 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2314 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2315 info.append("| {} | {:<7} | {:<18} |\n".format( 2316 "[{}] {}{}".format( 2317 curr, 2318 view["analytics"]["distrByCurrencies"][curr]["name"], 2319 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2320 ), 2321 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2322 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2323 )) 2324 2325 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2326 info.extend([ 2327 "\n## Portfolio distribution by countries\n" 2328 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2329 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2330 ]) 2331 2332 for country in view["analytics"]["distrByCountries"].keys(): 2333 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2334 nameLen = len(country) 2335 info.append("| {} | {:<7} | {:<18} |\n".format( 2336 "{}{}".format( 2337 country, 2338 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2339 ), 2340 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2341 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2342 )) 2343 2344 infoText = "".join(info) 2345 2346 uLogger.info(infoText) 2347 2348 if details == "full" and self.overviewFile: 2349 filename = self.overviewFile 2350 2351 elif details == "digest" and self.overviewDigestFile: 2352 filename = self.overviewDigestFile 2353 2354 elif details == "positions" and self.overviewPositionsFile: 2355 filename = self.overviewPositionsFile 2356 2357 elif details == "orders" and self.overviewOrdersFile: 2358 filename = self.overviewOrdersFile 2359 2360 elif details == "analytics" and self.overviewAnalyticsFile: 2361 filename = self.overviewAnalyticsFile 2362 2363 else: 2364 filename = "" 2365 2366 if filename: 2367 with open(filename, "w", encoding="UTF-8") as fH: 2368 fH.write(infoText) 2369 2370 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2371 2372 return view 2373 2374 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2375 """ 2376 Returns history operations between two given dates for current `accountId`. 2377 If `reportFile` string is not empty then also save human-readable report. 2378 Shows some statistical data of closed positions. 2379 2380 :param start: see docstring in `GetDatesAsString()` method 2381 :param end: see docstring in `GetDatesAsString()` method 2382 :param show: if `True` then also prints all records to the console. 2383 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2384 :return: original list of dictionaries with history of deals records from API ("operations" key): 2385 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2386 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2387 """ 2388 if self.accountId is None or not self.accountId: 2389 uLogger.error("Variable `accountId` must be defined for using this method!") 2390 raise Exception("Account ID required") 2391 2392 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2393 2394 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2395 2396 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2397 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2398 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2399 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2400 customStat = {} # custom statistics in additional to responseJSON 2401 2402 # --- output report in human-readable format: 2403 if show or self.reportFile: 2404 splitLine1 = "| | | | | |\n" # Summary section 2405 splitLine2 = "| | | | | | | | |\n" # Operations section 2406 nextDay = "" 2407 2408 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2409 2410 if len(ops) > 0: 2411 customStat = { 2412 "opsCount": 0, # total operations count 2413 "buyCount": 0, # buy operations 2414 "sellCount": 0, # sell operations 2415 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2416 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2417 "payIn": {"rub": 0.}, # Deposit brokerage account 2418 "payOut": {"rub": 0.}, # Withdrawals 2419 "divs": {"rub": 0.}, # Dividends income 2420 "coupons": {"rub": 0.}, # Coupon's income 2421 "brokerCom": {"rub": 0.}, # Service commissions 2422 "serviceCom": {"rub": 0.}, # Service commissions 2423 "marginCom": {"rub": 0.}, # Margin commissions 2424 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2425 } 2426 2427 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2428 for item in ops: 2429 if item["state"] == "OPERATION_STATE_EXECUTED": 2430 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2431 2432 # count buy operations: 2433 if "_BUY" in item["operationType"]: 2434 customStat["buyCount"] += 1 2435 2436 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2437 customStat["buyTotal"][item["payment"]["currency"]] += payment 2438 2439 else: 2440 customStat["buyTotal"][item["payment"]["currency"]] = payment 2441 2442 # count sell operations: 2443 elif "_SELL" in item["operationType"]: 2444 customStat["sellCount"] += 1 2445 2446 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2447 customStat["sellTotal"][item["payment"]["currency"]] += payment 2448 2449 else: 2450 customStat["sellTotal"][item["payment"]["currency"]] = payment 2451 2452 # count incoming operations: 2453 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2454 if item["payment"]["currency"] in customStat["payIn"].keys(): 2455 customStat["payIn"][item["payment"]["currency"]] += payment 2456 2457 else: 2458 customStat["payIn"][item["payment"]["currency"]] = payment 2459 2460 # count withdrawals operations: 2461 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2462 if item["payment"]["currency"] in customStat["payOut"].keys(): 2463 customStat["payOut"][item["payment"]["currency"]] += payment 2464 2465 else: 2466 customStat["payOut"][item["payment"]["currency"]] = payment 2467 2468 # count dividends income: 2469 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2470 if item["payment"]["currency"] in customStat["divs"].keys(): 2471 customStat["divs"][item["payment"]["currency"]] += payment 2472 2473 else: 2474 customStat["divs"][item["payment"]["currency"]] = payment 2475 2476 # count coupon's income: 2477 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2478 if item["payment"]["currency"] in customStat["coupons"].keys(): 2479 customStat["coupons"][item["payment"]["currency"]] += payment 2480 2481 else: 2482 customStat["coupons"][item["payment"]["currency"]] = payment 2483 2484 # count broker commissions: 2485 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2486 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2487 customStat["brokerCom"][item["payment"]["currency"]] += payment 2488 2489 else: 2490 customStat["brokerCom"][item["payment"]["currency"]] = payment 2491 2492 # count service commissions: 2493 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2494 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2495 customStat["serviceCom"][item["payment"]["currency"]] += payment 2496 2497 else: 2498 customStat["serviceCom"][item["payment"]["currency"]] = payment 2499 2500 # count margin commissions: 2501 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2502 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2503 customStat["marginCom"][item["payment"]["currency"]] += payment 2504 2505 else: 2506 customStat["marginCom"][item["payment"]["currency"]] = payment 2507 2508 # count withholding taxes: 2509 elif "_TAX" in item["operationType"]: 2510 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2511 customStat["allTaxes"][item["payment"]["currency"]] += payment 2512 2513 else: 2514 customStat["allTaxes"][item["payment"]["currency"]] = payment 2515 2516 else: 2517 continue 2518 2519 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2520 2521 # --- view "Actions" lines: 2522 info.extend([ 2523 "| 1 | 2 | 3 | 4 | 5 |\n", 2524 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2525 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2526 "| | Buy: {:<22} | {:<28} | | |\n".format( 2527 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2528 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2529 ), 2530 "| | Sell: {:<21} | {:<28} | | |\n".format( 2531 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2532 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2533 ), 2534 ]) 2535 2536 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2537 for key in opsKeys: 2538 if key == "rub": 2539 continue 2540 2541 info.extend([ 2542 "| | | {:<28} | | |\n".format( 2543 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2544 ), 2545 "| | | {:<28} | | |\n".format( 2546 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2547 ), 2548 ]) 2549 2550 info.append(splitLine1) 2551 2552 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2553 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2554 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2555 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2558 ) 2559 2560 # --- view "Payments" lines: 2561 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2562 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2563 2564 for key in paymentsKeys: 2565 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2566 2567 info.append(splitLine1) 2568 2569 # --- view "Commissions and taxes" lines: 2570 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2571 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2572 2573 for key in comKeys: 2574 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2575 2576 info.append(splitLine1) 2577 2578 info.extend([ 2579 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2580 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2581 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2582 ]) 2583 2584 else: 2585 info.append("Broker returned no operations during this period\n") 2586 2587 # --- view "Operations" section: 2588 for item in ops: 2589 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2590 continue 2591 2592 else: 2593 self.figi = item["figi"] if item["figi"] else "" 2594 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2595 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2596 2597 # group of deals during one day: 2598 if nextDay and item["date"].split("T")[0] != nextDay: 2599 info.append(splitLine2) 2600 nextDay = "" 2601 2602 else: 2603 nextDay = item["date"].split("T")[0] # saving current day for splitting 2604 2605 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2606 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2607 self.figi if self.figi else "—", 2608 instrument["ticker"] if instrument else "—", 2609 instrument["type"] if instrument else "—", 2610 item["quantity"] if int(item["quantity"]) > 0 else "—", 2611 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2612 TKS_OPERATION_STATES[item["state"]], 2613 TKS_OPERATION_TYPES[item["operationType"]], 2614 )) 2615 2616 infoText = "".join(info) 2617 2618 if show: 2619 uLogger.info(infoText) 2620 2621 if self.reportFile: 2622 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2623 fH.write(infoText) 2624 2625 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2626 2627 return ops, customStat 2628 2629 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2630 """ 2631 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2632 2633 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2634 Warning! Broker server used ISO UTC time by default. 2635 2636 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2637 Also, `historyFile` used to update history with `onlyMissing` parameter. 2638 2639 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2640 2641 :param start: see docstring in `GetDatesAsString()` method. 2642 :param end: see docstring in `GetDatesAsString()` method. 2643 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2644 `"hour"`, `"day"`. Default: `"hour"`. 2645 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2646 False by default. Warning! History appends only from last candle to current time 2647 with always update last candle! 2648 :param csvSep: separator if csv-file is used, `,` by default. 2649 :param show: if `True` then also prints Pandas DataFrame to the console. 2650 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2651 `["date", "time", "open", "high", "low", "close", "volume"]`. 2652 """ 2653 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2654 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2655 history = None # empty pandas object for history 2656 2657 if interval not in TKS_CANDLE_INTERVALS.keys(): 2658 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2659 raise Exception("Incorrect value") 2660 2661 if not (self.ticker or self.figi): 2662 uLogger.error("Ticker or FIGI must be defined!") 2663 raise Exception("Ticker or FIGI required") 2664 2665 if self.ticker and not self.figi: 2666 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2667 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2668 2669 if self.figi and not self.ticker: 2670 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2671 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2672 2673 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2674 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2675 if interval.lower() != "day": 2676 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2677 2678 delta = dtEnd - dtStart # current UTC time minus last time in file 2679 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2680 2681 # calculate history length in candles: 2682 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2683 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2684 length += 1 # to avoid fraction time 2685 2686 # calculate data blocks count: 2687 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2688 2689 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2690 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2691 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2692 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2693 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2694 2695 tempOld = None # pandas object for old history, if --only-missing key present 2696 lastTime = None # datetime object of last old candle in file 2697 2698 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2699 uLogger.debug("--only-missing key present, add only last missing candles...") 2700 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2701 2702 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2703 2704 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2705 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2706 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2707 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2708 2709 # get last datetime object from last string in file or minus 1 delta if file is empty: 2710 if len(tempOld) > 0: 2711 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2712 2713 else: 2714 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2715 2716 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2717 2718 responseJSONs = [] # raw history blocks of data 2719 2720 blockEnd = dtEnd 2721 for item in range(blocks): 2722 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2723 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2724 2725 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2726 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2727 )) 2728 2729 if blockStart == blockEnd: 2730 uLogger.debug("Skipped this zero-length block...") 2731 2732 else: 2733 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2734 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2735 self.body = str({ 2736 "figi": self.figi, 2737 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2738 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2739 "interval": TKS_CANDLE_INTERVALS[interval][0] 2740 }) 2741 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2742 2743 if "code" in responseJSON.keys(): 2744 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2745 2746 else: 2747 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2748 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2749 2750 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2751 2752 blockEnd = blockStart 2753 2754 printCount = len(responseJSONs) # candles to show in console 2755 if responseJSONs: 2756 tempHistory = pd.DataFrame( 2757 data={ 2758 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2759 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2761 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2762 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2763 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2764 "volume": [int(item["volume"]) for item in responseJSONs], 2765 }, 2766 index=range(len(responseJSONs)), 2767 columns=["date", "time", "open", "high", "low", "close", "volume"], 2768 ) 2769 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2770 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2771 2772 # append only newest candles to old history if --only-missing key present: 2773 if onlyMissing and tempOld is not None and lastTime is not None: 2774 index = 0 # find start index in tempHistory data: 2775 2776 for i, item in tempHistory.iterrows(): 2777 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2778 2779 if curTime == lastTime: 2780 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2781 index = i 2782 printCount = index + 1 2783 break 2784 2785 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2786 2787 else: 2788 history = tempHistory # if no `--only-missing` key then load full data from server 2789 2790 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2791 2792 if history is not None and not history.empty: 2793 if show: 2794 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2795 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2796 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2797 )) 2798 2799 else: 2800 uLogger.warning("Received an empty candles history!") 2801 2802 if self.historyFile is not None: 2803 if history is not None and not history.empty: 2804 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2805 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2806 2807 else: 2808 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2809 2810 else: 2811 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2812 2813 return history 2814 2815 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2816 """ 2817 Load candles history from csv-file and return Pandas DataFrame object. 2818 2819 See also: `History()` and `ShowHistoryChart()` methods. 2820 2821 :param filePath: path to csv-file to open. 2822 """ 2823 loadedHistory = None # init candles data object 2824 2825 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2826 2827 if os.path.exists(filePath): 2828 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2829 2830 tfStr = self.priceModel.FormattedDelta( 2831 self.priceModel.timeframe, 2832 "{days} days {hours}h {minutes}m {seconds}s", 2833 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2834 self.priceModel.timeframe, 2835 "{hours}h {minutes}m {seconds}s", 2836 ) 2837 2838 if loadedHistory is not None and not loadedHistory.empty: 2839 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2840 len(loadedHistory), 2841 tfStr, 2842 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2843 ) 2844 2845 else: 2846 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2847 2848 else: 2849 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2850 2851 return loadedHistory 2852 2853 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2854 """ 2855 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2856 2857 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2858 Default: `index.html` (both for interact and non-interact candlesticks chart). 2859 2860 See also: `History()` and `LoadHistory()` methods. 2861 2862 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2863 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2864 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2865 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2866 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2867 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2868 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2869 """ 2870 if isinstance(candles, str): 2871 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2872 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2873 2874 elif isinstance(candles, pd.DataFrame): 2875 self.priceModel.prices = candles # set candles chain from variable 2876 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2877 2878 if "datetime" not in candles.columns: 2879 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2880 2881 else: 2882 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2883 raise Exception("Incorrect value") 2884 2885 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2886 2887 if interact: 2888 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2889 2890 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2891 2892 else: 2893 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2894 2895 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2896 2897 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2898 2899 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2900 """ 2901 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2902 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2903 2904 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2905 2906 :param operation: string "Buy" or "Sell". 2907 :param lots: volume, integer count of lots >= 1. 2908 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2909 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2910 :param expDate: string "Undefined" by default or local date in future, 2911 it is a string with format `%Y-%m-%d %H:%M:%S`. 2912 :return: JSON with response from broker server. 2913 """ 2914 if self.accountId is None or not self.accountId: 2915 uLogger.error("Variable `accountId` must be defined for using this method!") 2916 raise Exception("Account ID required") 2917 2918 if operation is None or not operation or operation not in ("Buy", "Sell"): 2919 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2920 raise Exception("Incorrect value") 2921 2922 if lots is None or lots < 1: 2923 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2924 lots = 1 2925 2926 if tp is None or tp < 0: 2927 tp = 0 2928 2929 if sl is None or sl < 0: 2930 sl = 0 2931 2932 if expDate is None or not expDate: 2933 expDate = "Undefined" 2934 2935 if not (self.ticker or self.figi): 2936 uLogger.error("Ticker or FIGI must be defined!") 2937 raise Exception("Ticker or FIGI required") 2938 2939 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2940 self.ticker = instrument["ticker"] 2941 self.figi = instrument["figi"] 2942 2943 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2944 2945 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2946 self.body = str({ 2947 "figi": self.figi, 2948 "quantity": str(lots), 2949 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2950 "accountId": str(self.accountId), 2951 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2952 }) 2953 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2954 2955 if "orderId" in response.keys(): 2956 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2957 operation, response["orderId"], 2958 self.ticker, self.figi, lots, 2959 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2960 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2961 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2962 )) 2963 2964 else: 2965 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2966 2967 if tp > 0: 2968 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2969 2970 if sl > 0: 2971 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2972 2973 return response 2974 2975 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2976 """ 2977 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2978 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2979 2980 See also: `Order()` and `Trade()` docstrings. 2981 2982 :param lots: volume, integer count of lots >= 1. 2983 :param tp: float > 0, take profit price of stop-order. 2984 :param sl: float > 0, stop loss price of stop-order. 2985 :param expDate: it's a local date in future. 2986 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2987 :return: JSON with response from broker server. 2988 """ 2989 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2990 2991 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2992 """ 2993 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2994 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2995 2996 See also: `Order()` and `Trade()` docstrings. 2997 2998 :param lots: volume, integer count of lots >= 1. 2999 :param tp: float > 0, take profit price of stop-order. 3000 :param sl: float > 0, stop loss price of stop-order. 3001 :param expDate: it's a local date in the future. 3002 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3003 :return: JSON with response from broker server. 3004 """ 3005 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3006 3007 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3008 """ 3009 Close position of given instruments. 3010 3011 :param tickers: tickers list of instruments that must be closed. 3012 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3013 This avoids unnecessary downloading data from the server. 3014 """ 3015 if not tickers: 3016 uLogger.info("Tickers list is empty, nothing to close.") 3017 3018 else: 3019 if portfolio is None or not portfolio: 3020 portfolio = self.Overview(show=False) 3021 3022 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3023 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3024 3025 for ticker in tickers: 3026 if ticker not in allOpenedTickers: 3027 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3028 continue 3029 3030 # search open trade info about instrument by ticker: 3031 instrument = {} 3032 for iType in TKS_INSTRUMENTS: 3033 if instrument: 3034 break 3035 3036 for item in portfolio["stat"][iType]: 3037 if item["ticker"] == ticker: 3038 instrument = item 3039 break 3040 3041 if instrument: 3042 self.ticker = ticker 3043 self.figi = instrument["figi"] 3044 3045 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3046 self.ticker, 3047 self.figi, 3048 int(instrument["volume"]), 3049 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3050 )) 3051 3052 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3053 3054 if tradeLots > 0: 3055 if instrument["blocked"] > 0: 3056 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3057 instrument["blocked"], 3058 self.ticker, 3059 tradeLots, 3060 )) 3061 3062 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3063 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3064 3065 else: 3066 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3067 3068 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3069 """ 3070 Close all positions of given instruments with defined type. 3071 3072 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3073 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3074 This avoids unnecessary downloading data from the server. 3075 """ 3076 if iType not in TKS_INSTRUMENTS: 3077 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3078 3079 else: 3080 if portfolio is None or not portfolio: 3081 portfolio = self.Overview(show=False) 3082 3083 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3084 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3085 3086 if tickers and portfolio: 3087 self.CloseTrades(tickers, portfolio) 3088 3089 else: 3090 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3091 3092 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3093 """ 3094 Universal method to create market or limit orders with all available parameters for current `accountId`. 3095 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3096 3097 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3098 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3099 3100 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3101 then broker immediately open market order as you can do simple --buy or --sell operations! 3102 3103 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3104 When current price will go up or down to target price value then broker opens a limit order. 3105 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3106 3107 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3108 3109 :param operation: string "Buy" or "Sell". 3110 :param orderType: string "Limit" or "Stop". 3111 :param lots: volume, integer count of lots >= 1. 3112 :param targetPrice: target price > 0. This is open trade price for limit order. 3113 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3114 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3115 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3116 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3117 Stop loss order always executed by market price. 3118 :param expDate: string "Undefined" by default or local date in future. 3119 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3120 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3121 A limit order has no expiration date, it lasts until the end of the trading day. 3122 :return: JSON with response from broker server. 3123 """ 3124 if self.accountId is None or not self.accountId: 3125 uLogger.error("Variable `accountId` must be defined for using this method!") 3126 raise Exception("Account ID required") 3127 3128 if operation is None or not operation or operation not in ("Buy", "Sell"): 3129 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3130 raise Exception("Incorrect value") 3131 3132 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3133 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3134 raise Exception("Incorrect value") 3135 3136 if lots is None or lots < 1: 3137 uLogger.error("You must define trade volume > 0: integer count of lots!") 3138 raise Exception("Incorrect value") 3139 3140 if targetPrice is None or targetPrice <= 0: 3141 uLogger.error("Target price for limit-order must be greater than 0!") 3142 raise Exception("Incorrect value") 3143 3144 if limitPrice is None or limitPrice <= 0: 3145 limitPrice = targetPrice 3146 3147 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3148 stopType = "Limit" 3149 3150 if expDate is None or not expDate: 3151 expDate = "Undefined" 3152 3153 if not (self.ticker or self.figi): 3154 uLogger.error("Tocker or FIGI must be defined!") 3155 raise Exception("Ticker or FIGI required") 3156 3157 response = {} 3158 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3159 self.ticker = instrument["ticker"] 3160 self.figi = instrument["figi"] 3161 3162 if orderType == "Limit": 3163 uLogger.debug( 3164 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3165 self.ticker, self.figi, 3166 operation, lots, targetPrice, instrument["currency"], 3167 )) 3168 3169 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3170 self.body = str({ 3171 "figi": self.figi, 3172 "quantity": str(lots), 3173 "price": FloatToNano(targetPrice), 3174 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3175 "accountId": str(self.accountId), 3176 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3177 }) 3178 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3179 3180 if "orderId" in response.keys(): 3181 uLogger.info( 3182 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3183 response["orderId"], 3184 self.ticker, self.figi, 3185 operation, lots, targetPrice, instrument["currency"], 3186 )) 3187 3188 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3189 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3190 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3191 targetPrice, instrument["currency"], 3192 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3193 )) 3194 3195 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3196 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3197 targetPrice, instrument["currency"], 3198 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3199 )) 3200 3201 else: 3202 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3203 3204 if orderType == "Stop": 3205 uLogger.debug( 3206 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3207 self.ticker, self.figi, 3208 operation, lots, 3209 targetPrice, instrument["currency"], 3210 limitPrice, instrument["currency"], 3211 stopType, expDate, 3212 )) 3213 3214 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3215 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3216 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3217 3218 body = { 3219 "figi": self.figi, 3220 "quantity": str(lots), 3221 "price": FloatToNano(limitPrice), 3222 "stopPrice": FloatToNano(targetPrice), 3223 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3224 "accountId": str(self.accountId), 3225 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3226 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3227 } 3228 3229 if expDateUTC: 3230 body["expireDate"] = expDateUTC 3231 3232 self.body = str(body) 3233 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3234 3235 if "stopOrderId" in response.keys(): 3236 uLogger.info( 3237 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3238 response["stopOrderId"], 3239 self.ticker, self.figi, 3240 operation, lots, 3241 targetPrice, instrument["currency"], 3242 limitPrice, instrument["currency"], 3243 TKS_STOP_ORDER_TYPES[stopOrderType], 3244 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3245 )) 3246 3247 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3248 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3249 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3250 targetPrice, instrument["currency"], 3251 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3252 )) 3253 3254 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3255 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3256 targetPrice, instrument["currency"], 3257 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3258 )) 3259 3260 else: 3261 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3262 3263 return response 3264 3265 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3266 """ 3267 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3268 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3269 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3270 See also: `Order()` docstring. 3271 3272 :param lots: volume, integer count of lots >= 1. 3273 :param targetPrice: target price > 0. This is open trade price for limit order. 3274 :return: JSON with response from broker server. 3275 """ 3276 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3277 3278 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3279 """ 3280 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3281 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3282 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3283 target price value then broker opens a limit order. See also: `Order()` docstring. 3284 3285 :param lots: volume, integer count of lots >= 1. 3286 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3287 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3288 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3289 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3290 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3291 :param expDate: string "Undefined" by default or local date in future. 3292 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3293 This date is converting to UTC format for server. 3294 :return: JSON with response from broker server. 3295 """ 3296 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3297 3298 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3299 """ 3300 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3301 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3302 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3303 See also: `Order()` docstring. 3304 3305 :param lots: volume, integer count of lots >= 1. 3306 :param targetPrice: target price > 0. This is open trade price for limit order. 3307 :return: JSON with response from broker server. 3308 """ 3309 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3310 3311 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3312 """ 3313 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3314 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3315 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3316 target price value then broker opens a limit order. See also: `Order()` docstring. 3317 3318 :param lots: volume, integer count of lots >= 1. 3319 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3320 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3321 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3322 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3323 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3324 :param expDate: string "Undefined" by default or local date in future. 3325 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3326 This date is converting to UTC format for server. 3327 :return: JSON with response from broker server. 3328 """ 3329 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3330 3331 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3332 """ 3333 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3334 3335 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3336 :param allOrdersIDs: pre-received lists of all active pending orders. 3337 This avoids unnecessary downloading data from the server. 3338 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3339 """ 3340 if self.accountId is None or not self.accountId: 3341 uLogger.error("Variable `accountId` must be defined for using this method!") 3342 raise Exception("Account ID required") 3343 3344 if orderIDs: 3345 if allOrdersIDs is None or not allOrdersIDs: 3346 rawOrders = self.RequestPendingOrders() 3347 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3348 3349 if allStopOrdersIDs is None or not allStopOrdersIDs: 3350 rawStopOrders = self.RequestStopOrders() 3351 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3352 3353 for orderID in orderIDs: 3354 idInPendingOrders = orderID in allOrdersIDs 3355 idInStopOrders = orderID in allStopOrdersIDs 3356 3357 if not (idInPendingOrders or idInStopOrders): 3358 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3359 continue 3360 3361 else: 3362 if idInPendingOrders: 3363 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3364 3365 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3366 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3367 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3368 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3369 3370 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3371 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3372 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3373 3374 else: 3375 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3376 3377 elif idInStopOrders: 3378 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3379 3380 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3381 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3382 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3383 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3384 3385 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3386 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3387 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3388 3389 else: 3390 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3391 3392 else: 3393 continue 3394 3395 def CloseAllOrders(self) -> None: 3396 """ 3397 Gets a list of open pending and stop orders and cancel it all. 3398 """ 3399 rawOrders = self.RequestPendingOrders() 3400 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3401 lenOrders = len(allOrdersIDs) 3402 3403 rawStopOrders = self.RequestStopOrders() 3404 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3405 lenSOrders = len(allStopOrdersIDs) 3406 3407 if lenOrders > 0 or lenSOrders > 0: 3408 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3409 3410 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3411 3412 else: 3413 uLogger.info("Orders not found, nothing to cancel.") 3414 3415 def CloseAll(self, *args) -> None: 3416 """ 3417 Close all available (not blocked) opened trades and orders. 3418 3419 Also, you can select one or more keywords case-insensitive: 3420 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3421 3422 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3423 """ 3424 overview = self.Overview(show=False) # get all open trades info 3425 3426 if len(args) == 0: 3427 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3428 self.CloseAllOrders() # close all pending and stop orders 3429 3430 for iType in TKS_INSTRUMENTS: 3431 if iType != "Currencies": 3432 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3433 3434 else: 3435 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3436 lowerArgs = [x.lower() for x in args] 3437 3438 if "orders" in lowerArgs: 3439 self.CloseAllOrders() # close all pending and stop orders 3440 3441 for iType in TKS_INSTRUMENTS: 3442 if iType.lower() in lowerArgs and iType != "Currencies": 3443 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3444 3445 @staticmethod 3446 def ParseOrderParameters(operation, **inputParameters): 3447 """ 3448 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3449 3450 :param operation: string "Buy" or "Sell". 3451 :param inputParameters: this is dict of strings that looks like this 3452 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3453 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3454 "prices" key: one or more prices to open limit-orders 3455 Counts of values in lots and prices lists must be equals! 3456 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3457 """ 3458 # TODO: update order grid work with api v2 3459 pass 3460 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3461 # 3462 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3463 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3464 # raise Exception("Incorrect value") 3465 # 3466 # if "l" in inputParameters.keys(): 3467 # inputParameters["lots"] = inputParameters.pop("l") 3468 # 3469 # if "p" in inputParameters.keys(): 3470 # inputParameters["prices"] = inputParameters.pop("p") 3471 # 3472 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3473 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3474 # raise Exception("Incorrect value") 3475 # 3476 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3477 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3478 # 3479 # if len(lots) != len(prices): 3480 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3481 # raise Exception("Incorrect value") 3482 # 3483 # uLogger.debug("Extracted parameters for orders:") 3484 # uLogger.debug("lots = {}".format(lots)) 3485 # uLogger.debug("prices = {}".format(prices)) 3486 # 3487 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3488 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3489 # uLogger.debug("Order parameters: {}".format(result)) 3490 # 3491 # return result 3492 3493 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3494 """ 3495 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3496 3497 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3498 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3499 """ 3500 result = False 3501 msg = "Instrument not defined!" 3502 3503 if portfolio is None or not portfolio: 3504 portfolio = self.Overview(show=False) 3505 3506 if self.ticker: 3507 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3508 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3509 3510 for iType in TKS_INSTRUMENTS: 3511 for instrument in portfolio["stat"][iType]: 3512 if instrument["ticker"] == self.ticker: 3513 result = True 3514 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3515 break 3516 3517 elif self.figi: 3518 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3519 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3520 3521 for iType in TKS_INSTRUMENTS: 3522 for instrument in portfolio["stat"][iType]: 3523 if instrument["figi"] == self.figi: 3524 result = True 3525 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3526 break 3527 3528 else: 3529 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3530 3531 uLogger.debug(msg) 3532 3533 return result 3534 3535 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3536 """ 3537 Returns instrument is in the user's portfolio if it presents there. 3538 Instrument must be defined by `ticker` (highly priority) or `figi`. 3539 3540 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3541 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3542 """ 3543 result = None 3544 msg = "Instrument not defined!" 3545 3546 if portfolio is None or not portfolio: 3547 portfolio = self.Overview(show=False) 3548 3549 if self.ticker: 3550 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3551 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3552 3553 for iType in TKS_INSTRUMENTS: 3554 for instrument in portfolio["stat"][iType]: 3555 if instrument["ticker"] == self.ticker: 3556 result = instrument 3557 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3558 break 3559 3560 elif self.figi: 3561 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3562 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3563 3564 for iType in TKS_INSTRUMENTS: 3565 for instrument in portfolio["stat"][iType]: 3566 if instrument["figi"] == self.figi: 3567 result = instrument 3568 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3569 break 3570 3571 else: 3572 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3573 3574 uLogger.debug(msg) 3575 3576 return result 3577 3578 def RequestLimits(self) -> dict: 3579 """ 3580 Method for obtaining the available funds for withdrawal for current `accountId`. 3581 3582 See also: 3583 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3584 - `OverviewLimits()` method 3585 3586 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3587 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3588 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3589 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3590 """ 3591 if self.accountId is None or not self.accountId: 3592 uLogger.error("Variable `accountId` must be defined for using this method!") 3593 raise Exception("Account ID required") 3594 3595 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3596 3597 self.body = str({"accountId": self.accountId}) 3598 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3599 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3600 3601 uLogger.debug("Records about available funds for withdrawal successfully received") 3602 3603 return rawLimits 3604 3605 def OverviewLimits(self, show: bool = False) -> dict: 3606 """ 3607 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3608 3609 See also: `RequestLimits()`. 3610 3611 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3612 :return: dict with raw parsed data from server and some calculated statistics about it. 3613 """ 3614 if self.accountId is None or not self.accountId: 3615 uLogger.error("Variable `accountId` must be defined for using this method!") 3616 raise Exception("Account ID required") 3617 3618 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3619 3620 view = { 3621 "rawLimits": rawLimits, 3622 "limits": { # parsed data for every currency: 3623 "money": { # this is an array of portfolio currency positions 3624 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3625 }, 3626 "blocked": { # this is an array of blocked currency 3627 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3628 }, 3629 "blockedGuarantee": { # this is locked money under collateral for futures 3630 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3631 }, 3632 }, 3633 } 3634 3635 # --- Prepare text table with limits in human-readable format: 3636 if show: 3637 info = [ 3638 "# Withdrawal limits\n\n", 3639 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3640 "* **Account ID:** [{}]\n".format(self.accountId), 3641 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3642 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3643 ] 3644 3645 for curr in view["limits"]["money"].keys(): 3646 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3647 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3648 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3649 3650 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3651 "[{}]".format(curr), 3652 "{:.2f}".format(view["limits"]["money"][curr]), 3653 "{:.2f}".format(availableMoney), 3654 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3655 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3656 ) 3657 3658 if curr == "rub": 3659 info.insert(5, infoStr) # insert at first position in table and after headers 3660 3661 else: 3662 info.append(infoStr) 3663 3664 infoText = "".join(info) 3665 3666 uLogger.info(infoText) 3667 3668 if self.withdrawalLimitsFile: 3669 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3670 fH.write(infoText) 3671 3672 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3673 3674 return view 3675 3676 def RequestAccounts(self) -> dict: 3677 """ 3678 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3679 3680 See also: 3681 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3682 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3683 - `OverviewUserInfo()` method 3684 3685 :return: dict with raw data from server that contains accounts info. Example of dict: 3686 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3687 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3688 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3689 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3690 """ 3691 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3692 3693 self.body = str({}) 3694 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3695 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3696 3697 uLogger.debug("Records about available accounts successfully received") 3698 3699 return rawAccounts 3700 3701 def RequestUserInfo(self) -> dict: 3702 """ 3703 Method for requesting common user's information. 3704 3705 See also: 3706 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3707 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3708 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3709 - `OverviewUserInfo()` method 3710 3711 :return: dict with raw data from server that contains user's information. Example of dict: 3712 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3713 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3714 """ 3715 uLogger.debug("Requesting common user's information. Wait, please...") 3716 3717 self.body = str({}) 3718 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3719 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3720 3721 uLogger.debug("Records about current user successfully received") 3722 3723 return rawUserInfo 3724 3725 def RequestMarginStatus(self, accountId: str = None) -> dict: 3726 """ 3727 Method for requesting margin calculation for defined account ID. 3728 3729 See also: 3730 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3731 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3732 - `OverviewUserInfo()` method 3733 3734 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3735 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3736 Example of responses: 3737 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3738 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3739 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3740 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3741 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3742 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3743 """ 3744 if accountId is None or not accountId: 3745 if self.accountId is None or not self.accountId: 3746 uLogger.error("Variable `accountId` must be defined for using this method!") 3747 raise Exception("Account ID required") 3748 3749 else: 3750 accountId = self.accountId # use `self.accountId` (main ID) by default 3751 3752 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3753 3754 self.body = str({"accountId": accountId}) 3755 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3756 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3757 3758 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3759 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3760 rawMargin = {} 3761 3762 else: 3763 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3764 3765 return rawMargin 3766 3767 def RequestTariffLimits(self) -> dict: 3768 """ 3769 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3770 3771 See also: 3772 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3773 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3774 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3775 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3776 - `OverviewUserInfo()` method 3777 3778 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3779 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3780 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3781 """ 3782 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3783 3784 self.body = str({}) 3785 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3786 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3787 3788 uLogger.debug("Records with limits of current tariff successfully received") 3789 3790 return rawTariffLimits 3791 3792 def RequestBondCoupons(self, iJSON: dict) -> dict: 3793 """ 3794 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3795 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3796 All dates are in UTC timezone. 3797 3798 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3799 Documentation: 3800 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3801 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3802 3803 See also: `ExtendBondsData()`. 3804 3805 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3806 If raw iJSON is not data of bond then server returns an error [400] with message: 3807 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3808 :return: dictionary with bond payment calendar. Response example 3809 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3810 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3811 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3812 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3813 """ 3814 if iJSON["figi"] is None or not iJSON["figi"]: 3815 uLogger.error("FIGI must be defined for using this method!") 3816 raise Exception("FIGI required") 3817 3818 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3819 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3820 3821 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3822 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3823 self.figi, 3824 startDate, 3825 endDate, 3826 )) 3827 3828 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3829 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3830 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3831 3832 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3833 uLogger.warning("Instrument type is not bond!") 3834 3835 else: 3836 uLogger.debug("Records about bond payment calendar successfully received") 3837 3838 return calendar 3839 3840 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3841 """ 3842 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3843 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3844 coupon yields, current yields and some statistics etc. 3845 3846 WARNING! This is too long operation if a lot of bonds requested from broker server. 3847 3848 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3849 3850 :param instruments: list of strings with tickers or FIGIs. 3851 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3852 for further used by data scientists or stock analytics. 3853 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3854 In XLSX-file and Pandas DataFrame fields mean: 3855 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3856 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3857 """ 3858 if instruments is None or not instruments: 3859 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3860 raise Exception("Ticker or FIGI required") 3861 3862 if isinstance(instruments, str): 3863 instruments = [instruments] 3864 3865 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3866 3867 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3868 3869 iCount = len(uniqueInstruments) 3870 tooLong = iCount >= 20 3871 if tooLong: 3872 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3873 3874 bonds = None 3875 for i, self.figi in enumerate(uniqueInstruments): 3876 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3877 3878 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3879 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3880 rawBond = self.SearchByFIGI(requestPrice=True) 3881 3882 # Widen raw data with UTC current time (iData["actualDateTime"]): 3883 actualDate = datetime.now(tzutc()) 3884 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3885 3886 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3887 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3888 3889 # Replace some values with human-readable: 3890 iData["nominalCurrency"] = iData["nominal"]["currency"] 3891 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3892 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3893 iData["aciCurrency"] = iData["aciValue"]["currency"] 3894 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3895 iData["issueSize"] = int(iData["issueSize"]) 3896 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3897 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3898 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3899 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3900 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3901 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3902 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3903 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3904 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3905 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3906 3907 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3908 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3909 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3910 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3911 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3912 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3913 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3914 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3915 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3916 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3917 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3918 3919 # Widen raw data with calendar data from `rawCalendar` values: 3920 calendarData = [] 3921 for item in iData["rawCalendar"]["events"]: 3922 calendarData.append({ 3923 "couponDate": item["couponDate"], 3924 "couponNumber": int(item["couponNumber"]), 3925 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3926 "payCurrency": item["payOneBond"]["currency"], 3927 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3928 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3929 "couponStartDate": item["couponStartDate"], 3930 "couponEndDate": item["couponEndDate"], 3931 "couponPeriod": item["couponPeriod"], 3932 }) 3933 3934 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3935 if "maturityDate" not in iData.keys(): 3936 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3937 3938 # Widen raw data with Coupon Rate. 3939 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3940 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3941 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3942 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3943 3944 # Widen raw data with Yield to Maturity (YTM) on current date. 3945 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3946 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3947 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3948 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3949 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3950 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3951 3952 iData["calendar"] = calendarData # adds calendar at the end 3953 3954 # Remove not used data: 3955 iData.pop("uid") 3956 iData.pop("positionUid") 3957 iData.pop("currentPrice") 3958 iData.pop("rawCalendar") 3959 3960 colNames = list(iData.keys()) 3961 if bonds is None: 3962 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3963 3964 else: 3965 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3966 3967 else: 3968 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3969 3970 processed = round(100 * (i + 1) / iCount, 1) 3971 if tooLong and processed % 5 == 0: 3972 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3973 3974 else: 3975 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3976 3977 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3978 3979 # Saving bonds from Pandas DataFrame to XLSX sheet: 3980 if xlsx and self.bondsXLSXFile: 3981 with pd.ExcelWriter( 3982 path=self.bondsXLSXFile, 3983 date_format=TKS_DATE_FORMAT, 3984 datetime_format=TKS_DATE_TIME_FORMAT, 3985 mode="w", 3986 ) as writer: 3987 bonds.to_excel( 3988 writer, 3989 sheet_name="Extended bonds data", 3990 index=True, 3991 encoding="UTF-8", 3992 freeze_panes=(1, 1), 3993 ) # saving as XLSX-file with freeze first row and column as headers 3994 3995 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3996 3997 return bonds 3998 3999 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4000 """ 4001 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4002 4003 WARNING! This is too long operation if a lot of bonds requested from broker server. 4004 4005 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4006 4007 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4008 extended information about bonds: main info, current prices, bond payment calendar, 4009 coupon yields, current yields and some statistics etc. 4010 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4011 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4012 for further used by data scientists or stock analytics. 4013 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4014 """ 4015 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4016 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4017 4018 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4019 4020 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4021 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4022 calendar = None 4023 for bond in extBonds.iterrows(): 4024 for item in bond[1]["calendar"]: 4025 cData = { 4026 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4027 "couponDate": item["couponDate"], 4028 "figi": bond[1]["figi"], 4029 "ticker": bond[1]["ticker"], 4030 "name": bond[1]["name"], 4031 "couponNumber": item["couponNumber"], 4032 "payOneBond": item["payOneBond"], 4033 "payCurrency": item["payCurrency"], 4034 "couponType": item["couponType"], 4035 "couponPeriod": item["couponPeriod"], 4036 "fixDate": item["fixDate"], 4037 "couponStartDate": item["couponStartDate"], 4038 "couponEndDate": item["couponEndDate"], 4039 } 4040 4041 if calendar is None: 4042 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4043 4044 else: 4045 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4046 4047 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4048 4049 # Saving calendar from Pandas DataFrame to XLSX sheet: 4050 if xlsx: 4051 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4052 4053 with pd.ExcelWriter( 4054 path=xlsxCalendarFile, 4055 date_format=TKS_DATE_FORMAT, 4056 datetime_format=TKS_DATE_TIME_FORMAT, 4057 mode="w", 4058 ) as writer: 4059 humanReadable = calendar.copy(deep=True) 4060 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4061 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4062 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4063 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4064 humanReadable.columns = colNames # human-readable column names 4065 4066 humanReadable.to_excel( 4067 writer, 4068 sheet_name="Bond payments calendar", 4069 index=False, 4070 encoding="UTF-8", 4071 freeze_panes=(1, 2), 4072 ) # saving as XLSX-file with freeze first row and column as headers 4073 4074 del humanReadable # release df in memory 4075 4076 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4077 4078 return calendar 4079 4080 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4081 """ 4082 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4083 Also, creates Markdown file with calendar data, `calendar.md` by default. 4084 4085 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4086 4087 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4088 extended information about bonds: main info, current prices, bond payment calendar, 4089 coupon yields, current yields and some statistics etc. 4090 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4091 :param show: if `True` then also printing bonds payment calendar to the console, 4092 otherwise save to file `calendarFile` only. `False` by default. 4093 :return: multilines text in Markdown format with bonds payment calendar as a table. 4094 """ 4095 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4096 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4097 4098 infoText = "# Bond payments calendar\n\n" 4099 4100 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4101 4102 if not calendar.empty: 4103 splitLine = "| | | | | | | | | |\n" 4104 4105 info = [ 4106 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4107 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4108 ] 4109 4110 newMonth = False 4111 notOneBond = calendar["figi"].nunique() > 1 4112 for i, bond in enumerate(calendar.iterrows()): 4113 if newMonth and notOneBond: 4114 info.append(splitLine) 4115 4116 info.append( 4117 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4118 " √" if bond[1]["paid"] else " —", 4119 bond[1]["couponDate"].split("T")[0], 4120 bond[1]["figi"], 4121 bond[1]["ticker"], 4122 bond[1]["couponNumber"], 4123 "{} {}".format( 4124 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4125 bond[1]["payCurrency"], 4126 ), 4127 bond[1]["couponType"], 4128 bond[1]["couponPeriod"], 4129 bond[1]["fixDate"].split("T")[0], 4130 ) 4131 ) 4132 4133 if i < len(calendar.values) - 1: 4134 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4135 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4136 newMonth = False if curDate.month == nextDate.month else True 4137 4138 else: 4139 newMonth = False 4140 4141 infoText += "".join(info) 4142 4143 if show: 4144 uLogger.info("{}".format(infoText)) 4145 4146 if self.calendarFile is not None: 4147 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4148 fH.write(infoText) 4149 4150 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4151 4152 else: 4153 infoText += "No data\n" 4154 4155 return infoText 4156 4157 def OverviewAccounts(self, show: bool = False) -> dict: 4158 """ 4159 Method for parsing and show simple table with all available user accounts. 4160 4161 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4162 4163 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4164 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4165 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4166 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4167 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4168 "closed": "—", "access": "Full access" }, ...}}` 4169 """ 4170 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4171 4172 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4173 accounts = { 4174 item["id"]: { 4175 "type": TKS_ACCOUNT_TYPES[item["type"]], 4176 "name": item["name"], 4177 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4178 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4179 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4180 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4181 } for item in rawAccounts["accounts"] 4182 } 4183 4184 # Raw and parsed data with some fields replaced in "stat" section: 4185 view = { 4186 "rawAccounts": rawAccounts, 4187 "stat": accounts, 4188 } 4189 4190 # --- Prepare simple text table with only accounts data in human-readable format: 4191 if show: 4192 info = [ 4193 "# User accounts\n\n", 4194 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4195 "| Account ID | Type | Status | Name |\n", 4196 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4197 ] 4198 4199 for account in view["stat"].keys(): 4200 info.extend([ 4201 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4202 account, 4203 view["stat"][account]["type"], 4204 view["stat"][account]["status"], 4205 view["stat"][account]["name"], 4206 ) 4207 ]) 4208 4209 infoText = "".join(info) 4210 4211 uLogger.info(infoText) 4212 4213 if self.userAccountsFile: 4214 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4215 fH.write(infoText) 4216 4217 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4218 4219 return view 4220 4221 def OverviewUserInfo(self, show: bool = False) -> dict: 4222 """ 4223 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4224 4225 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4226 4227 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4228 :return: dict with raw parsed data from server and some calculated statistics about it. 4229 """ 4230 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4231 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4232 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4233 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4234 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4235 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4236 4237 # This is dict with parsed common user data: 4238 userInfo = { 4239 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4240 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4241 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4242 "tariff": rawUserInfo["tariff"], 4243 } 4244 4245 # This is an array of dict with parsed margin statuses for every account IDs: 4246 margins = {} 4247 for accountId in accounts.keys(): 4248 if rawMargins[accountId]: 4249 margins[accountId] = { 4250 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4251 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4252 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4253 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4254 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4255 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4256 } 4257 4258 else: 4259 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4260 4261 unary = {} # unary-connection limits 4262 for item in rawTariffLimits["unaryLimits"]: 4263 if item["limitPerMinute"] in unary.keys(): 4264 unary[item["limitPerMinute"]].extend(item["methods"]) 4265 4266 else: 4267 unary[item["limitPerMinute"]] = item["methods"] 4268 4269 stream = {} # stream-connection limits 4270 for item in rawTariffLimits["streamLimits"]: 4271 if item["limit"] in stream.keys(): 4272 stream[item["limit"]].extend(item["streams"]) 4273 4274 else: 4275 stream[item["limit"]] = item["streams"] 4276 4277 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4278 limits = { 4279 "unary": unary, 4280 "stream": stream, 4281 } 4282 4283 # Raw and parsed data as an output result: 4284 view = { 4285 "rawUserInfo": rawUserInfo, 4286 "rawAccounts": rawAccounts, 4287 "rawMargins": rawMargins, 4288 "rawTariffLimits": rawTariffLimits, 4289 "stat": { 4290 "userInfo": userInfo, 4291 "accounts": accounts, 4292 "margins": margins, 4293 "limits": limits, 4294 }, 4295 } 4296 4297 # --- Prepare text table with user information in human-readable format: 4298 if show: 4299 info = [ 4300 "# Full user information\n\n", 4301 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4302 "## Common information\n\n", 4303 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4304 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4305 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4306 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4307 "\n## User accounts\n\n", 4308 ] 4309 4310 for account in view["stat"]["accounts"].keys(): 4311 info.extend([ 4312 "### ID: [{}]\n\n".format(account), 4313 "| Parameters | Values |\n", 4314 "|----------------------|--------------------------------------------------------------|\n", 4315 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4316 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4317 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4318 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4319 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4320 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4321 ]) 4322 4323 if margins[account]: 4324 info.extend([ 4325 "| Margin status: | Enabled |\n", 4326 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4327 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4328 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4329 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4330 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4331 ]) 4332 4333 else: 4334 info.append("| Margin status: | Disabled |\n\n") 4335 4336 info.extend([ 4337 "\n## Current user tariff limits\n", 4338 "\nSee also:\n", 4339 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4340 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4341 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4342 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4343 "\n### Unary limits\n", 4344 ]) 4345 4346 if unary: 4347 for key, values in sorted(unary.items()): 4348 info.append("\n* Max requests per minute: {}\n".format(key)) 4349 4350 for value in values: 4351 info.append(" - {}\n".format(value)) 4352 4353 else: 4354 info.append("\nNot available\n") 4355 4356 info.append("\n### Stream limits\n") 4357 4358 if stream: 4359 for key, values in sorted(stream.items()): 4360 info.append("\n* Max stream connections: {}\n".format(key)) 4361 4362 for value in values: 4363 info.append(" - {}\n".format(value)) 4364 4365 else: 4366 info.append("\nNot available\n") 4367 4368 infoText = "".join(info) 4369 4370 uLogger.info(infoText) 4371 4372 if self.userInfoFile: 4373 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4374 fH.write(infoText) 4375 4376 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4377 4378 return view 4379 4380 4381class Args: 4382 """ 4383 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4384 """ 4385 def __init__(self, **kwargs): 4386 self.__dict__.update(kwargs) 4387 4388 def __getattr__(self, item): 4389 return None 4390 4391 4392def ParseArgs(): 4393 """This function get and parse command line keys.""" 4394 parser = ArgumentParser() # command-line string parser 4395 4396 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4397 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4398 4399 # --- options: 4400 4401 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4402 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4403 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4404 4405 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4406 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4407 4408 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4409 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4410 4411 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4412 4413 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4414 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4415 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4416 4417 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4418 4419 # --- commands: 4420 4421 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4422 4423 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4424 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4425 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4426 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4427 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4428 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4429 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4430 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4431 4432 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4433 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4434 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4435 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4436 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4437 4438 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4439 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4440 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4441 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4442 4443 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4444 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4445 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4446 4447 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4448 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4449 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4450 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4451 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4452 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4453 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4454 4455 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4456 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4457 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4458 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4459 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4460 4461 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4462 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4463 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4464 4465 cmdArgs = parser.parse_args() 4466 return cmdArgs 4467 4468 4469def Main(**kwargs): 4470 """ 4471 Main function for work with TKSBrokerAPI in the console. 4472 4473 See examples: 4474 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4475 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4476 """ 4477 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4478 4479 if args.debug_level: 4480 uLogger.level = 10 # always debug level by default 4481 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4482 4483 exitCode = 0 4484 start = datetime.now(tzutc()) 4485 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4486 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4487 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4488 )) 4489 4490 # trying to calculate full current version: 4491 buildVersion = __version__ 4492 try: 4493 v = version("tksbrokerapi") 4494 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4495 4496 except Exception: 4497 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4498 4499 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4500 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4501 4502 try: 4503 if args.version: 4504 print("TKSBrokerAPI {}".format(buildVersion)) 4505 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4506 4507 else: 4508 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4509 server = TinkoffBrokerServer( 4510 token=args.token, 4511 accountId=args.account_id, 4512 useCache=not args.no_cache, 4513 ) 4514 4515 # --- set some options: 4516 4517 if args.ticker: 4518 if args.ticker in server.aliasesKeys: 4519 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4520 4521 else: 4522 server.ticker = args.ticker 4523 4524 if args.figi: 4525 server.figi = args.figi 4526 4527 if args.depth is not None: 4528 server.depth = args.depth 4529 4530 # --- do one of commands: 4531 4532 if args.list: 4533 if args.output is not None: 4534 server.instrumentsFile = args.output 4535 4536 server.ShowInstrumentsInfo(show=True) 4537 4538 elif args.list_xlsx: 4539 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4540 4541 elif args.bonds_xlsx is not None: 4542 if args.output is not None: 4543 server.bondsXLSXFile = args.output 4544 4545 if len(args.bonds_xlsx) == 0: 4546 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4547 4548 else: 4549 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4550 4551 elif args.search: 4552 if args.output is not None: 4553 server.searchResultsFile = args.output 4554 4555 server.SearchInstruments(pattern=args.search[0], show=True) 4556 4557 elif args.info: 4558 if not (args.ticker or args.figi): 4559 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4560 raise Exception("Ticker or FIGI required") 4561 4562 if args.output is not None: 4563 server.infoFile = args.output 4564 4565 if args.ticker: 4566 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4567 4568 else: 4569 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4570 4571 elif args.calendar is not None: 4572 if args.output is not None: 4573 server.calendarFile = args.output 4574 4575 if len(args.calendar) == 0: 4576 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4577 4578 else: 4579 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4580 4581 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4582 4583 elif args.price: 4584 if not (args.ticker or args.figi): 4585 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4586 raise Exception("Ticker or FIGI required") 4587 4588 server.GetCurrentPrices(show=True) 4589 4590 elif args.prices is not None: 4591 if args.output is not None: 4592 server.pricesFile = args.output 4593 4594 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4595 4596 elif args.overview: 4597 if args.output is not None: 4598 server.overviewFile = args.output 4599 4600 server.Overview(show=True, details="full") 4601 4602 elif args.overview_digest: 4603 if args.output is not None: 4604 server.overviewDigestFile = args.output 4605 4606 server.Overview(show=True, details="digest") 4607 4608 elif args.overview_positions: 4609 if args.output is not None: 4610 server.overviewPositionsFile = args.output 4611 4612 server.Overview(show=True, details="positions") 4613 4614 elif args.overview_orders: 4615 if args.output is not None: 4616 server.overviewOrdersFile = args.output 4617 4618 server.Overview(show=True, details="orders") 4619 4620 elif args.overview_analytics: 4621 if args.output is not None: 4622 server.overviewAnalyticsFile = args.output 4623 4624 server.Overview(show=True, details="analytics") 4625 4626 elif args.deals is not None: 4627 if args.output is not None: 4628 server.reportFile = args.output 4629 4630 if 0 <= len(args.deals) < 3: 4631 server.Deals( 4632 start=args.deals[0] if len(args.deals) >= 1 else None, 4633 end=args.deals[1] if len(args.deals) == 2 else None, 4634 show=True, # Always show deals report in console 4635 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4636 ) 4637 4638 else: 4639 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4640 raise Exception("Incorrect value") 4641 4642 elif args.history is not None: 4643 if args.output is not None: 4644 server.historyFile = args.output 4645 4646 if 0 <= len(args.history) < 3: 4647 dataReceived = server.History( 4648 start=args.history[0] if len(args.history) >= 1 else None, 4649 end=args.history[1] if len(args.history) == 2 else None, 4650 interval="hour" if args.interval is None or not args.interval else args.interval, 4651 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4652 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4653 show=True, # shows all downloaded candles in console 4654 ) 4655 4656 if args.render_chart is not None and dataReceived is not None: 4657 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4658 4659 server.ShowHistoryChart( 4660 candles=dataReceived, 4661 interact=iChart, 4662 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4663 ) 4664 4665 else: 4666 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4667 raise Exception("Incorrect value") 4668 4669 elif args.load_history is not None: 4670 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4671 4672 if args.render_chart is not None and histData is not None: 4673 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4674 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4675 4676 server.ShowHistoryChart( 4677 candles=histData, 4678 interact=iChart, 4679 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4680 ) 4681 4682 elif args.trade is not None: 4683 if 1 <= len(args.trade) <= 5: 4684 server.Trade( 4685 operation=args.trade[0], 4686 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4687 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4688 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4689 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4690 ) 4691 4692 else: 4693 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4694 4695 elif args.buy is not None: 4696 if 0 <= len(args.buy) <= 4: 4697 server.Buy( 4698 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4699 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4700 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4701 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4702 ) 4703 4704 else: 4705 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4706 4707 elif args.sell is not None: 4708 if 0 <= len(args.sell) <= 4: 4709 server.Sell( 4710 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4711 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4712 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4713 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4714 ) 4715 4716 else: 4717 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4718 4719 elif args.order: 4720 if 4 <= len(args.order) <= 7: 4721 server.Order( 4722 operation=args.order[0], 4723 orderType=args.order[1], 4724 lots=int(args.order[2]), 4725 targetPrice=float(args.order[3]), 4726 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4727 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4728 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4729 ) 4730 4731 else: 4732 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4733 4734 elif args.buy_limit: 4735 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4736 4737 elif args.sell_limit: 4738 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4739 4740 elif args.buy_stop: 4741 if 2 <= len(args.buy_stop) <= 7: 4742 server.BuyStop( 4743 lots=int(args.buy_stop[0]), 4744 targetPrice=float(args.buy_stop[1]), 4745 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4746 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4747 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4748 ) 4749 4750 else: 4751 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4752 4753 elif args.sell_stop: 4754 if 2 <= len(args.sell_stop) <= 7: 4755 server.SellStop( 4756 lots=int(args.sell_stop[0]), 4757 targetPrice=float(args.sell_stop[1]), 4758 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4759 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4760 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4761 ) 4762 4763 else: 4764 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4765 4766 # elif args.buy_order_grid is not None: 4767 # # update order grid work with api v2 4768 # if len(args.buy_order_grid) == 2: 4769 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4770 # 4771 # for order in orderParams: 4772 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4773 # 4774 # else: 4775 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4776 # 4777 # elif args.sell_order_grid is not None: 4778 # # update order grid work with api v2 4779 # if len(args.sell_order_grid) >= 2: 4780 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4781 # 4782 # for order in orderParams: 4783 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4784 # 4785 # else: 4786 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4787 4788 elif args.close_order is not None: 4789 server.CloseOrders(args.close_order) # close only one order 4790 4791 elif args.close_orders is not None: 4792 server.CloseOrders(args.close_orders) # close list of orders 4793 4794 elif args.close_trade: 4795 if not args.ticker: 4796 uLogger.error("`--ticker` key is required for this operation!") 4797 raise Exception("Ticker required") 4798 4799 server.CloseTrades([args.ticker]) # close only one trade 4800 4801 elif args.close_trades is not None: 4802 server.CloseTrades(args.close_trades) # close trades for list of tickers 4803 4804 elif args.close_all is not None: 4805 server.CloseAll(*args.close_all) 4806 4807 elif args.limits: 4808 if args.output is not None: 4809 server.withdrawalLimitsFile = args.output 4810 4811 server.OverviewLimits(show=True) 4812 4813 elif args.user_info: 4814 if args.output is not None: 4815 server.userInfoFile = args.output 4816 4817 server.OverviewUserInfo(show=True) 4818 4819 elif args.account: 4820 if args.output is not None: 4821 server.userAccountsFile = args.output 4822 4823 server.OverviewAccounts(show=True) 4824 4825 else: 4826 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4827 raise Exception("There is no command to execute") 4828 4829 except Exception: 4830 trace = tb.format_exc() 4831 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4832 if e in trace: 4833 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4834 break 4835 4836 uLogger.debug(trace) 4837 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4838 exitCode = 255 # an error occurred, must be open a ticket for this issue 4839 4840 finally: 4841 finish = datetime.now(tzutc()) 4842 4843 if exitCode == 0: 4844 uLogger.debug("All operations were finished success (summary code is 0).") 4845 4846 else: 4847 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4848 os.path.abspath(uLog.defaultLogFile), exitCode, 4849 )) 4850 4851 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4852 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4853 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4854 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4855 )) 4856 4857 if not kwargs: 4858 sys.exit(exitCode) 4859 4860 else: 4861 return exitCode 4862 4863 4864if __name__ == "__main__": 4865 Main()
80def NanoToFloat(units: str, nano: int) -> float: 81 """ 82 Convert number in nano-view mode with string parameter `units` and integer parameter `nano` to float view. Examples: 83 84 `NanoToFloat(units="2", nano=500000000) -> 2.5` 85 86 `NanoToFloat(units="0", nano=50000000) -> 0.05` 87 88 :param units: integer string or integer parameter that represents the integer part of number 89 :param nano: integer string or integer parameter that represents the fractional part of number 90 :return: float view of number 91 """ 92 return int(units) + int(nano) * NANO
Convert number in nano-view mode with string parameter units and integer parameter nano to float view. Examples:
NanoToFloat(units="2", nano=500000000) -> 2.5
NanoToFloat(units="0", nano=50000000) -> 0.05
Parameters
- units: integer string or integer parameter that represents the integer part of number
- nano: integer string or integer parameter that represents the fractional part of number
Returns
float view of number
95def FloatToNano(number: float) -> dict: 96 """ 97 Convert float number to nano-type view: dictionary with string `units` and integer `nano` parameters `{"units": "string", "nano": integer}`. Examples: 98 99 `FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}` 100 101 `FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}` 102 103 :param number: float number 104 :return: nano-type view of number: `{"units": "string", "nano": integer}` 105 """ 106 splitByPoint = str(number).split(".") 107 frac = 0 108 109 if len(splitByPoint) > 1: 110 if len(splitByPoint[1]) <= 9: 111 frac = int("{}{}".format( 112 int(splitByPoint[1]), 113 "0" * (9 - len(splitByPoint[1])), 114 )) 115 116 if (number < 0) and (frac > 0): 117 frac = -frac 118 119 return {"units": str(int(number)), "nano": frac}
Convert float number to nano-type view: dictionary with string units and integer nano parameters {"units": "string", "nano": integer}. Examples:
FloatToNano(number=2.5) -> {"units": "2", "nano": 500000000}
FloatToNano(number=0.05) -> {"units": "0", "nano": 50000000}
Parameters
- number: float number
Returns
nano-type view of number:
{"units": "string", "nano": integer}
122def GetDatesAsString(start: str = None, end: str = None) -> tuple: 123 """ 124 Create tuple of date and time strings with timezone parsed from user-friendly date. 125 126 User dates format must be like: `%Y-%m-%d`, e.g. `2020-02-03` (3 Feb, 2020). 127 128 Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") 129 An error exception will occur if input date has incorrect format. 130 131 If `start=None`, `end=None` then return dates from yesterday to the end of the day. 132 If `start=some_date_1`, `end=None` then return dates from `some_date_1` to the end of the day. 133 If `start=some_date_1`, `end=some_date_2` then return dates from start of `some_date_1` to end of `some_date_2`. 134 Start day may be negative integer numbers: `-1`, `-2`, `-3` - how many days ago. 135 136 Also, you can use keywords for start if `end=None`: 137 `today` (from 00:00:00 to the end of current day), 138 `yesterday` (-1 day from 00:00:00 to 23:59:59), 139 `week` (-7 day from 00:00:00 to the end of current day), 140 `month` (-30 day from 00:00:00 to the end of current day), 141 `year` (-365 day from 00:00:00 to the end of current day), 142 143 :return: tuple with 2 strings `(start, end)` dates in UTC ISO time format `%Y-%m-%dT%H:%M:%SZ` for OpenAPI. 144 See date and time format here: `TKSEnums.TKS_DATE_TIME_FORMAT`. 145 Example: `("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z")`. Second string is the end of the last day. 146 """ 147 uLogger.debug("Input start day is [{}] (UTC), end day is [{}] (UTC)".format(start, end)) 148 s = datetime.now(tzutc()).replace(hour=0, minute=0, second=0, microsecond=0) # start of the current day 149 e = s.replace(hour=23, minute=59, second=59, microsecond=0) # end of the current day 150 151 # time between start and the end of the current day: 152 if start is None or start.lower() == "today": 153 pass 154 155 # from start of the last day to the end of the last day: 156 elif start.lower() == "yesterday": 157 s -= timedelta(days=1) 158 e -= timedelta(days=1) 159 160 # week (-7 day from 00:00:00 to the end of the current day): 161 elif start.lower() == "week": 162 s -= timedelta(days=6) # +1 current day already taken into account 163 164 # month (-30 day from 00:00:00 to the end of current day): 165 elif start.lower() == "month": 166 s -= timedelta(days=29) # +1 current day already taken into account 167 168 # year (-365 day from 00:00:00 to the end of current day): 169 elif start.lower() == "year": 170 s -= timedelta(days=364) # +1 current day already taken into account 171 172 # -N days ago to the end of current day: 173 elif start.startswith('-') and start[1:].isdigit(): 174 s -= timedelta(days=abs(int(start)) - 1) # +1 current day already taken into account 175 176 # dates between start day at 00:00:00 and the end of the last day at 23:59:59: 177 else: 178 s = datetime.strptime(start, "%Y-%m-%d").replace(hour=0, minute=0, second=0, microsecond=0, tzinfo=tzutc()) 179 e = datetime.strptime(end, "%Y-%m-%d").replace(hour=23, minute=59, second=59, microsecond=0, tzinfo=tzutc()) if end is not None else e 180 181 # converting to UTC ISO time formatted with Z suffix for Tinkoff Open API: 182 s = s.strftime(TKS_DATE_TIME_FORMAT) 183 e = e.strftime(TKS_DATE_TIME_FORMAT) 184 185 uLogger.debug("Start day converted to UTC ISO format, with Z: [{}], and the end day: [{}]".format(s, e)) 186 187 return s, e
Create tuple of date and time strings with timezone parsed from user-friendly date.
User dates format must be like: %Y-%m-%d, e.g. 2020-02-03 (3 Feb, 2020).
Example input: "2022-06-01" "2022-06-20" -> output: ("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z") An error exception will occur if input date has incorrect format.
If start=None, end=None then return dates from yesterday to the end of the day.
If start=some_date_1, end=None then return dates from some_date_1 to the end of the day.
If start=some_date_1, end=some_date_2 then return dates from start of some_date_1 to end of some_date_2.
Start day may be negative integer numbers: -1, -2, -3 - how many days ago.
Also, you can use keywords for start if end=None:
today (from 00:00:00 to the end of current day),
yesterday (-1 day from 00:00:00 to 23:59:59),
week (-7 day from 00:00:00 to the end of current day),
month (-30 day from 00:00:00 to the end of current day),
year (-365 day from 00:00:00 to the end of current day),
Returns
tuple with 2 strings
(start, end)dates in UTC ISO time format%Y-%m-%dT%H:%M:%SZfor OpenAPI. See date and time format here:TKSEnums.TKS_DATE_TIME_FORMAT. Example:("2022-06-01T00:00:00Z", "2022-06-20T23:59:59Z"). Second string is the end of the last day.
190class TinkoffBrokerServer: 191 """ 192 This class implements methods to work with Tinkoff broker server. 193 194 Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/ 195 196 About `token`: https://tinkoff.github.io/investAPI/token/ 197 """ 198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """ 449 450 @staticmethod 451 def _ParseJSON(rawData="{}", debug: bool = False) -> dict: 452 """ 453 Parse JSON from response string. 454 455 :param rawData: this is a string with JSON-formatted text. 456 :param debug: if `True` then print more debug information. 457 :return: JSON (dictionary), parsed from server response string. 458 """ 459 if debug: 460 uLogger.debug("Raw text body:") 461 uLogger.debug(rawData) 462 463 responseJSON = json.loads(rawData) if rawData else {} 464 465 if debug: 466 uLogger.debug("JSON formatted:") 467 for jsonLine in json.dumps(responseJSON, indent=4).split('\n'): 468 uLogger.debug(jsonLine) 469 470 return responseJSON 471 472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON 559 560 def _IUpdater(self, iType: str) -> tuple: 561 """ 562 Request instrument by type from server. See available API methods for instruments: 563 Currencies: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Currencies 564 Shares: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Shares 565 Bonds: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Bonds 566 Etfs: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Etfs 567 Futures: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_Futures 568 569 :param iType: type of the instrument, it must be one of supported types in TKS_INSTRUMENTS list. 570 :return: tuple with iType name and list of available instruments of current type for defined user token. 571 """ 572 result = [] 573 574 if iType in TKS_INSTRUMENTS: 575 uLogger.debug("Requesting available [{}] list. Wait, please...".format(iType)) 576 577 # all instruments have the same body in API v2 requests: 578 self.body = str({"instrumentStatus": "INSTRUMENT_STATUS_UNSPECIFIED"}) # Enum: [INSTRUMENT_STATUS_UNSPECIFIED, INSTRUMENT_STATUS_BASE, INSTRUMENT_STATUS_ALL] 579 instrumentURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/{}".format(iType) 580 result = self.SendAPIRequest(instrumentURL, reqType="POST", debug=False)["instruments"] 581 582 return iType, result 583 584 def _IWrapper(self, kwargs): 585 """ 586 Wrapper runs instrument's update method `_IUpdater()`. 587 It's a workaround for using multiprocessing with kwargs. See: https://stackoverflow.com/a/36799206 588 """ 589 return self._IUpdater(**kwargs) 590 591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList 627 628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile))) 668 669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump 694 695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText 936 937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON 1007 1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON 1103 1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices 1238 1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText 1292 1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults 1371 1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs 1416 1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList 1444 1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText 1494 1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus 1518 1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio 1540 1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions 1562 1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders 1584 1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders 1606 1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view 2374 2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat 2629 2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history 2815 2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory 2853 2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile))) 2899 2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response 2975 2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate) 2991 2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate) 3007 3008 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param tickers: tickers list of instruments that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if not tickers: 3017 uLogger.info("Tickers list is empty, nothing to close.") 3018 3019 else: 3020 if portfolio is None or not portfolio: 3021 portfolio = self.Overview(show=False) 3022 3023 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3024 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3025 3026 for ticker in tickers: 3027 if ticker not in allOpenedTickers: 3028 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3029 continue 3030 3031 # search open trade info about instrument by ticker: 3032 instrument = {} 3033 for iType in TKS_INSTRUMENTS: 3034 if instrument: 3035 break 3036 3037 for item in portfolio["stat"][iType]: 3038 if item["ticker"] == ticker: 3039 instrument = item 3040 break 3041 3042 if instrument: 3043 self.ticker = ticker 3044 self.figi = instrument["figi"] 3045 3046 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3047 self.ticker, 3048 self.figi, 3049 int(instrument["volume"]), 3050 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3051 )) 3052 3053 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3054 3055 if tradeLots > 0: 3056 if instrument["blocked"] > 0: 3057 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3058 instrument["blocked"], 3059 self.ticker, 3060 tradeLots, 3061 )) 3062 3063 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3064 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3065 3066 else: 3067 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker)) 3068 3069 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3070 """ 3071 Close all positions of given instruments with defined type. 3072 3073 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if iType not in TKS_INSTRUMENTS: 3078 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3079 3080 else: 3081 if portfolio is None or not portfolio: 3082 portfolio = self.Overview(show=False) 3083 3084 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3085 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3086 3087 if tickers and portfolio: 3088 self.CloseTrades(tickers, portfolio) 3089 3090 else: 3091 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType)) 3092 3093 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3094 """ 3095 Universal method to create market or limit orders with all available parameters for current `accountId`. 3096 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3097 3098 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3099 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3100 3101 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3102 then broker immediately open market order as you can do simple --buy or --sell operations! 3103 3104 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3105 When current price will go up or down to target price value then broker opens a limit order. 3106 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3107 3108 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3109 3110 :param operation: string "Buy" or "Sell". 3111 :param orderType: string "Limit" or "Stop". 3112 :param lots: volume, integer count of lots >= 1. 3113 :param targetPrice: target price > 0. This is open trade price for limit order. 3114 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3115 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3116 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3117 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3118 Stop loss order always executed by market price. 3119 :param expDate: string "Undefined" by default or local date in future. 3120 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3121 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3122 A limit order has no expiration date, it lasts until the end of the trading day. 3123 :return: JSON with response from broker server. 3124 """ 3125 if self.accountId is None or not self.accountId: 3126 uLogger.error("Variable `accountId` must be defined for using this method!") 3127 raise Exception("Account ID required") 3128 3129 if operation is None or not operation or operation not in ("Buy", "Sell"): 3130 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3131 raise Exception("Incorrect value") 3132 3133 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3134 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3135 raise Exception("Incorrect value") 3136 3137 if lots is None or lots < 1: 3138 uLogger.error("You must define trade volume > 0: integer count of lots!") 3139 raise Exception("Incorrect value") 3140 3141 if targetPrice is None or targetPrice <= 0: 3142 uLogger.error("Target price for limit-order must be greater than 0!") 3143 raise Exception("Incorrect value") 3144 3145 if limitPrice is None or limitPrice <= 0: 3146 limitPrice = targetPrice 3147 3148 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3149 stopType = "Limit" 3150 3151 if expDate is None or not expDate: 3152 expDate = "Undefined" 3153 3154 if not (self.ticker or self.figi): 3155 uLogger.error("Tocker or FIGI must be defined!") 3156 raise Exception("Ticker or FIGI required") 3157 3158 response = {} 3159 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3160 self.ticker = instrument["ticker"] 3161 self.figi = instrument["figi"] 3162 3163 if orderType == "Limit": 3164 uLogger.debug( 3165 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3166 self.ticker, self.figi, 3167 operation, lots, targetPrice, instrument["currency"], 3168 )) 3169 3170 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3171 self.body = str({ 3172 "figi": self.figi, 3173 "quantity": str(lots), 3174 "price": FloatToNano(targetPrice), 3175 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3176 "accountId": str(self.accountId), 3177 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3178 }) 3179 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3180 3181 if "orderId" in response.keys(): 3182 uLogger.info( 3183 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3184 response["orderId"], 3185 self.ticker, self.figi, 3186 operation, lots, targetPrice, instrument["currency"], 3187 )) 3188 3189 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3190 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3197 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3198 targetPrice, instrument["currency"], 3199 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3200 )) 3201 3202 else: 3203 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3204 3205 if orderType == "Stop": 3206 uLogger.debug( 3207 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3208 self.ticker, self.figi, 3209 operation, lots, 3210 targetPrice, instrument["currency"], 3211 limitPrice, instrument["currency"], 3212 stopType, expDate, 3213 )) 3214 3215 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3216 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3217 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3218 3219 body = { 3220 "figi": self.figi, 3221 "quantity": str(lots), 3222 "price": FloatToNano(limitPrice), 3223 "stopPrice": FloatToNano(targetPrice), 3224 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3225 "accountId": str(self.accountId), 3226 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3227 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3228 } 3229 3230 if expDateUTC: 3231 body["expireDate"] = expDateUTC 3232 3233 self.body = str(body) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3235 3236 if "stopOrderId" in response.keys(): 3237 uLogger.info( 3238 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3239 response["stopOrderId"], 3240 self.ticker, self.figi, 3241 operation, lots, 3242 targetPrice, instrument["currency"], 3243 limitPrice, instrument["currency"], 3244 TKS_STOP_ORDER_TYPES[stopOrderType], 3245 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3256 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3263 3264 return response 3265 3266 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3267 """ 3268 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3269 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3270 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3271 See also: `Order()` docstring. 3272 3273 :param lots: volume, integer count of lots >= 1. 3274 :param targetPrice: target price > 0. This is open trade price for limit order. 3275 :return: JSON with response from broker server. 3276 """ 3277 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice) 3278 3279 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3280 """ 3281 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3282 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3283 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3284 target price value then broker opens a limit order. See also: `Order()` docstring. 3285 3286 :param lots: volume, integer count of lots >= 1. 3287 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3288 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3289 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3290 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3291 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3292 :param expDate: string "Undefined" by default or local date in future. 3293 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3294 This date is converting to UTC format for server. 3295 :return: JSON with response from broker server. 3296 """ 3297 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3298 3299 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3300 """ 3301 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3302 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3303 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3304 See also: `Order()` docstring. 3305 3306 :param lots: volume, integer count of lots >= 1. 3307 :param targetPrice: target price > 0. This is open trade price for limit order. 3308 :return: JSON with response from broker server. 3309 """ 3310 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice) 3311 3312 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3313 """ 3314 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3315 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3316 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3317 target price value then broker opens a limit order. See also: `Order()` docstring. 3318 3319 :param lots: volume, integer count of lots >= 1. 3320 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3321 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3322 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3323 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3324 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3325 :param expDate: string "Undefined" by default or local date in future. 3326 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3327 This date is converting to UTC format for server. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate) 3331 3332 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3333 """ 3334 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3335 3336 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3337 :param allOrdersIDs: pre-received lists of all active pending orders. 3338 This avoids unnecessary downloading data from the server. 3339 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3340 """ 3341 if self.accountId is None or not self.accountId: 3342 uLogger.error("Variable `accountId` must be defined for using this method!") 3343 raise Exception("Account ID required") 3344 3345 if orderIDs: 3346 if allOrdersIDs is None or not allOrdersIDs: 3347 rawOrders = self.RequestPendingOrders() 3348 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3349 3350 if allStopOrdersIDs is None or not allStopOrdersIDs: 3351 rawStopOrders = self.RequestStopOrders() 3352 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3353 3354 for orderID in orderIDs: 3355 idInPendingOrders = orderID in allOrdersIDs 3356 idInStopOrders = orderID in allStopOrdersIDs 3357 3358 if not (idInPendingOrders or idInStopOrders): 3359 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3360 continue 3361 3362 else: 3363 if idInPendingOrders: 3364 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3365 3366 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3367 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3368 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3369 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3370 3371 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3372 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3373 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3374 3375 else: 3376 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3377 3378 elif idInStopOrders: 3379 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3380 3381 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3382 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3383 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3384 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3385 3386 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3387 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3388 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3389 3390 else: 3391 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3392 3393 else: 3394 continue 3395 3396 def CloseAllOrders(self) -> None: 3397 """ 3398 Gets a list of open pending and stop orders and cancel it all. 3399 """ 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3402 lenOrders = len(allOrdersIDs) 3403 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 lenSOrders = len(allStopOrdersIDs) 3407 3408 if lenOrders > 0 or lenSOrders > 0: 3409 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3410 3411 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3412 3413 else: 3414 uLogger.info("Orders not found, nothing to cancel.") 3415 3416 def CloseAll(self, *args) -> None: 3417 """ 3418 Close all available (not blocked) opened trades and orders. 3419 3420 Also, you can select one or more keywords case-insensitive: 3421 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3422 3423 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3424 """ 3425 overview = self.Overview(show=False) # get all open trades info 3426 3427 if len(args) == 0: 3428 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3429 self.CloseAllOrders() # close all pending and stop orders 3430 3431 for iType in TKS_INSTRUMENTS: 3432 if iType != "Currencies": 3433 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3434 3435 else: 3436 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3437 lowerArgs = [x.lower() for x in args] 3438 3439 if "orders" in lowerArgs: 3440 self.CloseAllOrders() # close all pending and stop orders 3441 3442 for iType in TKS_INSTRUMENTS: 3443 if iType.lower() in lowerArgs and iType != "Currencies": 3444 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3445 3446 @staticmethod 3447 def ParseOrderParameters(operation, **inputParameters): 3448 """ 3449 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3450 3451 :param operation: string "Buy" or "Sell". 3452 :param inputParameters: this is dict of strings that looks like this 3453 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3454 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3455 "prices" key: one or more prices to open limit-orders 3456 Counts of values in lots and prices lists must be equals! 3457 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3458 """ 3459 # TODO: update order grid work with api v2 3460 pass 3461 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3462 # 3463 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3464 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3465 # raise Exception("Incorrect value") 3466 # 3467 # if "l" in inputParameters.keys(): 3468 # inputParameters["lots"] = inputParameters.pop("l") 3469 # 3470 # if "p" in inputParameters.keys(): 3471 # inputParameters["prices"] = inputParameters.pop("p") 3472 # 3473 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3474 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3475 # raise Exception("Incorrect value") 3476 # 3477 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3478 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3479 # 3480 # if len(lots) != len(prices): 3481 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3482 # raise Exception("Incorrect value") 3483 # 3484 # uLogger.debug("Extracted parameters for orders:") 3485 # uLogger.debug("lots = {}".format(lots)) 3486 # uLogger.debug("prices = {}".format(prices)) 3487 # 3488 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3489 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3490 # uLogger.debug("Order parameters: {}".format(result)) 3491 # 3492 # return result 3493 3494 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3495 """ 3496 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3497 3498 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3499 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3500 """ 3501 result = False 3502 msg = "Instrument not defined!" 3503 3504 if portfolio is None or not portfolio: 3505 portfolio = self.Overview(show=False) 3506 3507 if self.ticker: 3508 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3509 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3510 3511 for iType in TKS_INSTRUMENTS: 3512 for instrument in portfolio["stat"][iType]: 3513 if instrument["ticker"] == self.ticker: 3514 result = True 3515 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3516 break 3517 3518 elif self.figi: 3519 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3520 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3521 3522 for iType in TKS_INSTRUMENTS: 3523 for instrument in portfolio["stat"][iType]: 3524 if instrument["figi"] == self.figi: 3525 result = True 3526 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3527 break 3528 3529 else: 3530 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3531 3532 uLogger.debug(msg) 3533 3534 return result 3535 3536 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3537 """ 3538 Returns instrument is in the user's portfolio if it presents there. 3539 Instrument must be defined by `ticker` (highly priority) or `figi`. 3540 3541 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3542 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3543 """ 3544 result = None 3545 msg = "Instrument not defined!" 3546 3547 if portfolio is None or not portfolio: 3548 portfolio = self.Overview(show=False) 3549 3550 if self.ticker: 3551 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3552 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3553 3554 for iType in TKS_INSTRUMENTS: 3555 for instrument in portfolio["stat"][iType]: 3556 if instrument["ticker"] == self.ticker: 3557 result = instrument 3558 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3559 break 3560 3561 elif self.figi: 3562 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3563 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3564 3565 for iType in TKS_INSTRUMENTS: 3566 for instrument in portfolio["stat"][iType]: 3567 if instrument["figi"] == self.figi: 3568 result = instrument 3569 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3570 break 3571 3572 else: 3573 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3574 3575 uLogger.debug(msg) 3576 3577 return result 3578 3579 def RequestLimits(self) -> dict: 3580 """ 3581 Method for obtaining the available funds for withdrawal for current `accountId`. 3582 3583 See also: 3584 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3585 - `OverviewLimits()` method 3586 3587 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3588 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3589 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3590 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3591 """ 3592 if self.accountId is None or not self.accountId: 3593 uLogger.error("Variable `accountId` must be defined for using this method!") 3594 raise Exception("Account ID required") 3595 3596 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3597 3598 self.body = str({"accountId": self.accountId}) 3599 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3600 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3601 3602 uLogger.debug("Records about available funds for withdrawal successfully received") 3603 3604 return rawLimits 3605 3606 def OverviewLimits(self, show: bool = False) -> dict: 3607 """ 3608 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3609 3610 See also: `RequestLimits()`. 3611 3612 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3613 :return: dict with raw parsed data from server and some calculated statistics about it. 3614 """ 3615 if self.accountId is None or not self.accountId: 3616 uLogger.error("Variable `accountId` must be defined for using this method!") 3617 raise Exception("Account ID required") 3618 3619 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3620 3621 view = { 3622 "rawLimits": rawLimits, 3623 "limits": { # parsed data for every currency: 3624 "money": { # this is an array of portfolio currency positions 3625 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3626 }, 3627 "blocked": { # this is an array of blocked currency 3628 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3629 }, 3630 "blockedGuarantee": { # this is locked money under collateral for futures 3631 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3632 }, 3633 }, 3634 } 3635 3636 # --- Prepare text table with limits in human-readable format: 3637 if show: 3638 info = [ 3639 "# Withdrawal limits\n\n", 3640 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3641 "* **Account ID:** [{}]\n".format(self.accountId), 3642 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3643 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3644 ] 3645 3646 for curr in view["limits"]["money"].keys(): 3647 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3648 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3649 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3650 3651 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3652 "[{}]".format(curr), 3653 "{:.2f}".format(view["limits"]["money"][curr]), 3654 "{:.2f}".format(availableMoney), 3655 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3656 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3657 ) 3658 3659 if curr == "rub": 3660 info.insert(5, infoStr) # insert at first position in table and after headers 3661 3662 else: 3663 info.append(infoStr) 3664 3665 infoText = "".join(info) 3666 3667 uLogger.info(infoText) 3668 3669 if self.withdrawalLimitsFile: 3670 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3671 fH.write(infoText) 3672 3673 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3674 3675 return view 3676 3677 def RequestAccounts(self) -> dict: 3678 """ 3679 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3680 3681 See also: 3682 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3683 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3684 - `OverviewUserInfo()` method 3685 3686 :return: dict with raw data from server that contains accounts info. Example of dict: 3687 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3688 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3689 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3690 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3691 """ 3692 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3693 3694 self.body = str({}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3696 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 uLogger.debug("Records about available accounts successfully received") 3699 3700 return rawAccounts 3701 3702 def RequestUserInfo(self) -> dict: 3703 """ 3704 Method for requesting common user's information. 3705 3706 See also: 3707 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3708 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3709 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3710 - `OverviewUserInfo()` method 3711 3712 :return: dict with raw data from server that contains user's information. Example of dict: 3713 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3714 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3715 """ 3716 uLogger.debug("Requesting common user's information. Wait, please...") 3717 3718 self.body = str({}) 3719 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3720 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3721 3722 uLogger.debug("Records about current user successfully received") 3723 3724 return rawUserInfo 3725 3726 def RequestMarginStatus(self, accountId: str = None) -> dict: 3727 """ 3728 Method for requesting margin calculation for defined account ID. 3729 3730 See also: 3731 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3732 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3733 - `OverviewUserInfo()` method 3734 3735 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3736 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3737 Example of responses: 3738 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3739 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3740 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3741 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3742 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3743 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3744 """ 3745 if accountId is None or not accountId: 3746 if self.accountId is None or not self.accountId: 3747 uLogger.error("Variable `accountId` must be defined for using this method!") 3748 raise Exception("Account ID required") 3749 3750 else: 3751 accountId = self.accountId # use `self.accountId` (main ID) by default 3752 3753 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3754 3755 self.body = str({"accountId": accountId}) 3756 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3757 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3758 3759 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3760 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3761 rawMargin = {} 3762 3763 else: 3764 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3765 3766 return rawMargin 3767 3768 def RequestTariffLimits(self) -> dict: 3769 """ 3770 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3771 3772 See also: 3773 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3774 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3775 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3776 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3777 - `OverviewUserInfo()` method 3778 3779 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3780 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3781 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3782 """ 3783 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3784 3785 self.body = str({}) 3786 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3787 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3788 3789 uLogger.debug("Records with limits of current tariff successfully received") 3790 3791 return rawTariffLimits 3792 3793 def RequestBondCoupons(self, iJSON: dict) -> dict: 3794 """ 3795 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3796 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3797 All dates are in UTC timezone. 3798 3799 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3800 Documentation: 3801 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3802 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3803 3804 See also: `ExtendBondsData()`. 3805 3806 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3807 If raw iJSON is not data of bond then server returns an error [400] with message: 3808 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3809 :return: dictionary with bond payment calendar. Response example 3810 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3811 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3812 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3813 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3814 """ 3815 if iJSON["figi"] is None or not iJSON["figi"]: 3816 uLogger.error("FIGI must be defined for using this method!") 3817 raise Exception("FIGI required") 3818 3819 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3820 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3821 3822 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3823 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3824 self.figi, 3825 startDate, 3826 endDate, 3827 )) 3828 3829 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3830 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3831 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3832 3833 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3834 uLogger.warning("Instrument type is not bond!") 3835 3836 else: 3837 uLogger.debug("Records about bond payment calendar successfully received") 3838 3839 return calendar 3840 3841 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3842 """ 3843 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3844 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3845 coupon yields, current yields and some statistics etc. 3846 3847 WARNING! This is too long operation if a lot of bonds requested from broker server. 3848 3849 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3850 3851 :param instruments: list of strings with tickers or FIGIs. 3852 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3853 for further used by data scientists or stock analytics. 3854 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3855 In XLSX-file and Pandas DataFrame fields mean: 3856 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3857 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3858 """ 3859 if instruments is None or not instruments: 3860 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3861 raise Exception("Ticker or FIGI required") 3862 3863 if isinstance(instruments, str): 3864 instruments = [instruments] 3865 3866 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3867 3868 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3869 3870 iCount = len(uniqueInstruments) 3871 tooLong = iCount >= 20 3872 if tooLong: 3873 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3874 3875 bonds = None 3876 for i, self.figi in enumerate(uniqueInstruments): 3877 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3878 3879 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3880 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3881 rawBond = self.SearchByFIGI(requestPrice=True) 3882 3883 # Widen raw data with UTC current time (iData["actualDateTime"]): 3884 actualDate = datetime.now(tzutc()) 3885 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3886 3887 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3888 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3889 3890 # Replace some values with human-readable: 3891 iData["nominalCurrency"] = iData["nominal"]["currency"] 3892 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3893 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3894 iData["aciCurrency"] = iData["aciValue"]["currency"] 3895 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3896 iData["issueSize"] = int(iData["issueSize"]) 3897 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3898 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3899 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3900 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3901 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3902 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3903 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3904 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3905 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3906 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3907 3908 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3909 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3910 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3911 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3912 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3913 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3914 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3915 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3916 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3917 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3918 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3919 3920 # Widen raw data with calendar data from `rawCalendar` values: 3921 calendarData = [] 3922 for item in iData["rawCalendar"]["events"]: 3923 calendarData.append({ 3924 "couponDate": item["couponDate"], 3925 "couponNumber": int(item["couponNumber"]), 3926 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3927 "payCurrency": item["payOneBond"]["currency"], 3928 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3929 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3930 "couponStartDate": item["couponStartDate"], 3931 "couponEndDate": item["couponEndDate"], 3932 "couponPeriod": item["couponPeriod"], 3933 }) 3934 3935 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3936 if "maturityDate" not in iData.keys(): 3937 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3938 3939 # Widen raw data with Coupon Rate. 3940 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3941 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3942 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3943 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3944 3945 # Widen raw data with Yield to Maturity (YTM) on current date. 3946 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3947 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3948 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3949 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3950 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3951 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3952 3953 iData["calendar"] = calendarData # adds calendar at the end 3954 3955 # Remove not used data: 3956 iData.pop("uid") 3957 iData.pop("positionUid") 3958 iData.pop("currentPrice") 3959 iData.pop("rawCalendar") 3960 3961 colNames = list(iData.keys()) 3962 if bonds is None: 3963 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3964 3965 else: 3966 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3967 3968 else: 3969 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3970 3971 processed = round(100 * (i + 1) / iCount, 1) 3972 if tooLong and processed % 5 == 0: 3973 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3974 3975 else: 3976 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3977 3978 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3979 3980 # Saving bonds from Pandas DataFrame to XLSX sheet: 3981 if xlsx and self.bondsXLSXFile: 3982 with pd.ExcelWriter( 3983 path=self.bondsXLSXFile, 3984 date_format=TKS_DATE_FORMAT, 3985 datetime_format=TKS_DATE_TIME_FORMAT, 3986 mode="w", 3987 ) as writer: 3988 bonds.to_excel( 3989 writer, 3990 sheet_name="Extended bonds data", 3991 index=True, 3992 encoding="UTF-8", 3993 freeze_panes=(1, 1), 3994 ) # saving as XLSX-file with freeze first row and column as headers 3995 3996 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3997 3998 return bonds 3999 4000 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4001 """ 4002 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4003 4004 WARNING! This is too long operation if a lot of bonds requested from broker server. 4005 4006 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4007 4008 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4009 extended information about bonds: main info, current prices, bond payment calendar, 4010 coupon yields, current yields and some statistics etc. 4011 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4012 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4013 for further used by data scientists or stock analytics. 4014 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4015 """ 4016 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4017 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4018 4019 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4020 4021 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4022 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4023 calendar = None 4024 for bond in extBonds.iterrows(): 4025 for item in bond[1]["calendar"]: 4026 cData = { 4027 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4028 "couponDate": item["couponDate"], 4029 "figi": bond[1]["figi"], 4030 "ticker": bond[1]["ticker"], 4031 "name": bond[1]["name"], 4032 "couponNumber": item["couponNumber"], 4033 "payOneBond": item["payOneBond"], 4034 "payCurrency": item["payCurrency"], 4035 "couponType": item["couponType"], 4036 "couponPeriod": item["couponPeriod"], 4037 "fixDate": item["fixDate"], 4038 "couponStartDate": item["couponStartDate"], 4039 "couponEndDate": item["couponEndDate"], 4040 } 4041 4042 if calendar is None: 4043 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4044 4045 else: 4046 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4047 4048 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4049 4050 # Saving calendar from Pandas DataFrame to XLSX sheet: 4051 if xlsx: 4052 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4053 4054 with pd.ExcelWriter( 4055 path=xlsxCalendarFile, 4056 date_format=TKS_DATE_FORMAT, 4057 datetime_format=TKS_DATE_TIME_FORMAT, 4058 mode="w", 4059 ) as writer: 4060 humanReadable = calendar.copy(deep=True) 4061 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4062 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4063 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4064 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4065 humanReadable.columns = colNames # human-readable column names 4066 4067 humanReadable.to_excel( 4068 writer, 4069 sheet_name="Bond payments calendar", 4070 index=False, 4071 encoding="UTF-8", 4072 freeze_panes=(1, 2), 4073 ) # saving as XLSX-file with freeze first row and column as headers 4074 4075 del humanReadable # release df in memory 4076 4077 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4078 4079 return calendar 4080 4081 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4082 """ 4083 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4084 Also, creates Markdown file with calendar data, `calendar.md` by default. 4085 4086 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4087 4088 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4089 extended information about bonds: main info, current prices, bond payment calendar, 4090 coupon yields, current yields and some statistics etc. 4091 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4092 :param show: if `True` then also printing bonds payment calendar to the console, 4093 otherwise save to file `calendarFile` only. `False` by default. 4094 :return: multilines text in Markdown format with bonds payment calendar as a table. 4095 """ 4096 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4097 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4098 4099 infoText = "# Bond payments calendar\n\n" 4100 4101 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4102 4103 if not calendar.empty: 4104 splitLine = "| | | | | | | | | |\n" 4105 4106 info = [ 4107 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4108 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4109 ] 4110 4111 newMonth = False 4112 notOneBond = calendar["figi"].nunique() > 1 4113 for i, bond in enumerate(calendar.iterrows()): 4114 if newMonth and notOneBond: 4115 info.append(splitLine) 4116 4117 info.append( 4118 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4119 " √" if bond[1]["paid"] else " —", 4120 bond[1]["couponDate"].split("T")[0], 4121 bond[1]["figi"], 4122 bond[1]["ticker"], 4123 bond[1]["couponNumber"], 4124 "{} {}".format( 4125 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4126 bond[1]["payCurrency"], 4127 ), 4128 bond[1]["couponType"], 4129 bond[1]["couponPeriod"], 4130 bond[1]["fixDate"].split("T")[0], 4131 ) 4132 ) 4133 4134 if i < len(calendar.values) - 1: 4135 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4136 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4137 newMonth = False if curDate.month == nextDate.month else True 4138 4139 else: 4140 newMonth = False 4141 4142 infoText += "".join(info) 4143 4144 if show: 4145 uLogger.info("{}".format(infoText)) 4146 4147 if self.calendarFile is not None: 4148 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4149 fH.write(infoText) 4150 4151 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4152 4153 else: 4154 infoText += "No data\n" 4155 4156 return infoText 4157 4158 def OverviewAccounts(self, show: bool = False) -> dict: 4159 """ 4160 Method for parsing and show simple table with all available user accounts. 4161 4162 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4163 4164 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4165 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4166 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4167 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4168 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4169 "closed": "—", "access": "Full access" }, ...}}` 4170 """ 4171 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4172 4173 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4174 accounts = { 4175 item["id"]: { 4176 "type": TKS_ACCOUNT_TYPES[item["type"]], 4177 "name": item["name"], 4178 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4179 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4180 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4181 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4182 } for item in rawAccounts["accounts"] 4183 } 4184 4185 # Raw and parsed data with some fields replaced in "stat" section: 4186 view = { 4187 "rawAccounts": rawAccounts, 4188 "stat": accounts, 4189 } 4190 4191 # --- Prepare simple text table with only accounts data in human-readable format: 4192 if show: 4193 info = [ 4194 "# User accounts\n\n", 4195 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4196 "| Account ID | Type | Status | Name |\n", 4197 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4198 ] 4199 4200 for account in view["stat"].keys(): 4201 info.extend([ 4202 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4203 account, 4204 view["stat"][account]["type"], 4205 view["stat"][account]["status"], 4206 view["stat"][account]["name"], 4207 ) 4208 ]) 4209 4210 infoText = "".join(info) 4211 4212 uLogger.info(infoText) 4213 4214 if self.userAccountsFile: 4215 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4216 fH.write(infoText) 4217 4218 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4219 4220 return view 4221 4222 def OverviewUserInfo(self, show: bool = False) -> dict: 4223 """ 4224 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4225 4226 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4227 4228 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4229 :return: dict with raw parsed data from server and some calculated statistics about it. 4230 """ 4231 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4232 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4233 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4234 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4235 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4236 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4237 4238 # This is dict with parsed common user data: 4239 userInfo = { 4240 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4241 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4242 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4243 "tariff": rawUserInfo["tariff"], 4244 } 4245 4246 # This is an array of dict with parsed margin statuses for every account IDs: 4247 margins = {} 4248 for accountId in accounts.keys(): 4249 if rawMargins[accountId]: 4250 margins[accountId] = { 4251 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4252 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4253 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4254 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4255 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4256 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4257 } 4258 4259 else: 4260 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4261 4262 unary = {} # unary-connection limits 4263 for item in rawTariffLimits["unaryLimits"]: 4264 if item["limitPerMinute"] in unary.keys(): 4265 unary[item["limitPerMinute"]].extend(item["methods"]) 4266 4267 else: 4268 unary[item["limitPerMinute"]] = item["methods"] 4269 4270 stream = {} # stream-connection limits 4271 for item in rawTariffLimits["streamLimits"]: 4272 if item["limit"] in stream.keys(): 4273 stream[item["limit"]].extend(item["streams"]) 4274 4275 else: 4276 stream[item["limit"]] = item["streams"] 4277 4278 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4279 limits = { 4280 "unary": unary, 4281 "stream": stream, 4282 } 4283 4284 # Raw and parsed data as an output result: 4285 view = { 4286 "rawUserInfo": rawUserInfo, 4287 "rawAccounts": rawAccounts, 4288 "rawMargins": rawMargins, 4289 "rawTariffLimits": rawTariffLimits, 4290 "stat": { 4291 "userInfo": userInfo, 4292 "accounts": accounts, 4293 "margins": margins, 4294 "limits": limits, 4295 }, 4296 } 4297 4298 # --- Prepare text table with user information in human-readable format: 4299 if show: 4300 info = [ 4301 "# Full user information\n\n", 4302 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4303 "## Common information\n\n", 4304 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4305 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4306 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4307 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4308 "\n## User accounts\n\n", 4309 ] 4310 4311 for account in view["stat"]["accounts"].keys(): 4312 info.extend([ 4313 "### ID: [{}]\n\n".format(account), 4314 "| Parameters | Values |\n", 4315 "|----------------------|--------------------------------------------------------------|\n", 4316 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4317 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4318 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4319 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4320 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4321 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4322 ]) 4323 4324 if margins[account]: 4325 info.extend([ 4326 "| Margin status: | Enabled |\n", 4327 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4328 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4329 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4330 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4331 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4332 ]) 4333 4334 else: 4335 info.append("| Margin status: | Disabled |\n\n") 4336 4337 info.extend([ 4338 "\n## Current user tariff limits\n", 4339 "\nSee also:\n", 4340 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4341 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4342 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4343 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4344 "\n### Unary limits\n", 4345 ]) 4346 4347 if unary: 4348 for key, values in sorted(unary.items()): 4349 info.append("\n* Max requests per minute: {}\n".format(key)) 4350 4351 for value in values: 4352 info.append(" - {}\n".format(value)) 4353 4354 else: 4355 info.append("\nNot available\n") 4356 4357 info.append("\n### Stream limits\n") 4358 4359 if stream: 4360 for key, values in sorted(stream.items()): 4361 info.append("\n* Max stream connections: {}\n".format(key)) 4362 4363 for value in values: 4364 info.append(" - {}\n".format(value)) 4365 4366 else: 4367 info.append("\nNot available\n") 4368 4369 infoText = "".join(info) 4370 4371 uLogger.info(infoText) 4372 4373 if self.userInfoFile: 4374 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4375 fH.write(infoText) 4376 4377 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4378 4379 return view
This class implements methods to work with Tinkoff broker server.
Examples to work with API: https://tinkoff.github.io/investAPI/swagger-ui/
About token: https://tinkoff.github.io/investAPI/token/
198 def __init__(self, token: str, accountId: str = None, useCache: bool = True, defaultCache: str = "dump.json") -> None: 199 """ 200 Main class init. 201 202 :param token: Bearer token for Tinkoff Invest API. It can be set from environment variable `TKS_API_TOKEN`. 203 :param accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports. 204 Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`. 205 :param useCache: use default cache file with raw data to use instead of `iList`. 206 True by default. Cache is auto-update if new day has come. 207 If you don't want to use cache and always updates raw data then set `useCache=False`. 208 :param defaultCache: path to default cache file. `dump.json` by default. 209 """ 210 if token is None or not token: 211 try: 212 self.token = r"{}".format(os.environ["TKS_API_TOKEN"]) 213 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from environment variable `TKS_API_TOKEN`. See https://tinkoff.github.io/investAPI/token/") 214 215 except KeyError: 216 uLogger.error("`--token` key or environment variable `TKS_API_TOKEN` is required! See https://tinkoff.github.io/investAPI/token/") 217 raise Exception("Token required") 218 219 else: 220 self.token = token # highly priority than environment variable 'TKS_API_TOKEN' 221 uLogger.debug("Bearer token for Tinkoff OpenAPI set up from class variable `token`") 222 223 if accountId is None or not accountId: 224 try: 225 self.accountId = r"{}".format(os.environ["TKS_ACCOUNT_ID"]) 226 uLogger.debug("Main account ID [{}] set up from environment variable `TKS_ACCOUNT_ID`".format(self.accountId)) 227 228 except KeyError: 229 uLogger.warning("`--account-id` key or environment variable `TKS_ACCOUNT_ID` undefined! Some of operations may be unavailable (overview, trading etc).") 230 231 else: 232 self.accountId = accountId # highly priority than environment variable 'TKS_ACCOUNT_ID' 233 uLogger.debug("Main account ID [{}] set up from class variable `accountId`".format(self.accountId)) 234 235 self.version = __version__ # duplicate here used TKSBrokerAPI main version 236 """Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only. 237 238 Latest version: https://pypi.org/project/tksbrokerapi/ 239 """ 240 241 self.aliases = TKS_TICKER_ALIASES 242 """Some aliases instead official tickers. 243 244 See also: `TKSEnums.TKS_TICKER_ALIASES` 245 """ 246 247 self.aliasesKeys = self.aliases.keys() # re-calc only first time at class init 248 249 self.exclude = TKS_TICKERS_OR_FIGI_EXCLUDED # some tickers or FIGIs raised exception earlier when it sends to server, that is why we exclude there 250 251 self.ticker = "" 252 """String with ticker, e.g. `GOOGL`. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR` etc. More tickers aliases here: `TKSEnums.TKS_TICKER_ALIASES`. 253 254 See also: `SearchByTicker()`, `SearchInstruments()`. 255 """ 256 257 self.figi = "" 258 """String with FIGI, e.g. ticker `GOOGL` has FIGI `BBG009S39JX6`. 259 260 See also: `SearchByFIGI()`, `SearchInstruments()`. 261 """ 262 263 self.depth = 1 264 """Depth of Market (DOM) can be >= 1. Default: 1. It used with `--price` key to showing DOM with current prices for givens ticker or FIGI. 265 266 See also: `GetCurrentPrices()`. 267 """ 268 269 self.server = r"https://invest-public-api.tinkoff.ru/rest" 270 """Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest 271 272 See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and `SendAPIRequest()`. 273 """ 274 275 uLogger.debug("Broker API server: {}".format(self.server)) 276 277 self.timeout = 15 278 """Server operations timeout in seconds. Default: `15`. 279 280 See also: `SendAPIRequest()`. 281 """ 282 283 self.headers = { 284 "Content-Type": "application/json", 285 "accept": "application/json", 286 "Authorization": "Bearer {}".format(self.token), 287 "x-app-name": "Tim55667757.TKSBrokerAPI", 288 } 289 """Headers which send in every request to broker server. Please, do not change it! Default: `{"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}`. 290 291 See also: `SendAPIRequest()`. 292 """ 293 294 self.body = None 295 """Request body which send to broker server. Default: `None`. 296 297 See also: `SendAPIRequest()`. 298 """ 299 300 self.historyFile = None 301 """Full path to the output file where history candles will be saved or updated. Default: `None`, it mean that returns only Pandas DataFrame. 302 303 See also: `History()`. 304 """ 305 306 self.htmlHistoryFile = "index.html" 307 """Full path to the html file where rendered candles chart stored. Default: `index.html`. 308 309 See also: `ShowHistoryChart()`. 310 """ 311 312 self.instrumentsFile = "instruments.md" 313 """Filename where full available to user instruments list will be saved. Default: `instruments.md`. 314 315 See also: `ShowInstrumentsInfo()`. 316 """ 317 318 self.searchResultsFile = "search-results.md" 319 """Filename with all found instruments searched by part of its ticker, FIGI or name. Default: `search-results.md`. 320 321 See also: `SearchInstruments()`. 322 """ 323 324 self.pricesFile = "prices.md" 325 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 326 327 See also: `GetListOfPrices()`. 328 """ 329 330 self.infoFile = "info.md" 331 """Filename where prices of selected instruments will be saved. Default: `prices.md`. 332 333 See also: `ShowInstrumentsInfo()`, `RequestBondCoupons()` and `RequestTradingStatus()`. 334 """ 335 336 self.bondsXLSXFile = "ext-bonds.xlsx" 337 """Filename where wider Pandas DataFrame with more information about bonds: main info, current prices, 338 bonds payment calendar, some statistics will be stored. Default: `ext-bonds.xlsx`. 339 340 See also: `ExtendBondsData()`. 341 """ 342 343 self.calendarFile = "calendar.md" 344 """Filename where bonds payment calendar will be saved. Default: `calendar.md`. 345 346 Pandas dataframe with only bonds payment calendar also will be stored to default file `calendar.xlsx`. 347 348 See also: `CreateBondsCalendar()`, `ShowBondsCalendar()`, `ShowInstrumentInfo()`, `RequestBondCoupons()` and `ExtendBondsData()`. 349 """ 350 351 self.overviewFile = "overview.md" 352 """Filename where current portfolio, open trades and orders will be saved. Default: `overview.md`. 353 354 See also: `Overview()`, `RequestPortfolio()`, `RequestPositions()`, `RequestPendingOrders()` and `RequestStopOrders()`. 355 """ 356 357 self.overviewDigestFile = "overview-digest.md" 358 """Filename where short digest of the portfolio status will be saved. Default: `overview-digest.md`. 359 360 See also: `Overview()` with parameter `details="digest"`. 361 """ 362 363 self.overviewPositionsFile = "overview-positions.md" 364 """Filename where only open positions, without everything else will be saved. Default: `overview-positions.md`. 365 366 See also: `Overview()` with parameter `details="positions"`. 367 """ 368 369 self.overviewOrdersFile = "overview-orders.md" 370 """Filename where open limits and stop orders will be saved. Default: `overview-orders.md`. 371 372 See also: `Overview()` with parameter `details="orders"`. 373 """ 374 375 self.overviewAnalyticsFile = "overview-analytics.md" 376 """Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: `overview-analytics.md`. 377 378 See also: `Overview()` with parameter `details="analytics"`. 379 """ 380 381 self.reportFile = "deals.md" 382 """Filename where history of deals and trade statistics will be saved. Default: `deals.md`. 383 384 See also: `Deals()`. 385 """ 386 387 self.withdrawalLimitsFile = "limits.md" 388 """Filename where table of funds available for withdrawal will be saved. Default: `limits.md`. 389 390 See also: `OverviewLimits()` and `RequestLimits()`. 391 """ 392 393 self.userInfoFile = "user-info.md" 394 """Filename where all available user's data (`accountId`s, common user information, margin status and tariff connections limit) will be saved. Default: `user-info.md`. 395 396 See also: `OverviewUserInfo()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()`. 397 """ 398 399 self.userAccountsFile = "accounts.md" 400 """Filename where simple table with all available user accounts (`accountId`s) will be saved. Default: `accounts.md`. 401 402 See also: `OverviewAccounts()`, `RequestAccounts()`. 403 """ 404 405 self.iListDumpFile = "dump.json" if defaultCache is None or not isinstance(defaultCache, str) or not defaultCache else defaultCache 406 """Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: `dump.json`. 407 408 Pandas dataframe with raw instruments data also will be stored to default file `dump.xlsx`. 409 410 See also: `DumpInstruments()` and `DumpInstrumentsAsXLSX()`. 411 """ 412 413 self.iList = None # init iList for raw instruments data 414 """Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the `iListDumpFile`. 415 416 See also: `Listing()`, `DumpInstruments()`. 417 """ 418 419 # trying to re-load raw instruments data from file `iListDumpFile` or try to update it from server: 420 if useCache: 421 if os.path.exists(self.iListDumpFile): 422 dumpTime = datetime.fromtimestamp(os.path.getmtime(self.iListDumpFile)).astimezone(tzutc()) # dump modification date and time 423 curTime = datetime.now(tzutc()) 424 425 if (curTime.day > dumpTime.day) or (curTime.month > dumpTime.month) or (curTime.year > dumpTime.year): 426 uLogger.warning("Local cache may be outdated! It has last modified [{}] UTC. Updating from broker server, wait, please...".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 427 428 self.DumpInstruments(forceUpdate=True) # updating self.iList and dump file 429 430 else: 431 self.iList = json.load(open(self.iListDumpFile, mode="r", encoding="UTF-8")) # load iList from dump 432 433 uLogger.debug("Local cache with raw instruments data is used: [{}]".format(os.path.abspath(self.iListDumpFile))) 434 uLogger.debug("Dump file was last modified [{}] UTC".format(dumpTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 435 436 else: 437 uLogger.warning("Local cache with raw instruments data not exists! Creating new dump, wait, please...") 438 self.DumpInstruments(forceUpdate=True) # updating self.iList and creating default dump file 439 440 else: 441 self.iList = self.Listing() # request new raw instruments data from broker server 442 self.DumpInstruments(forceUpdate=False) # save raw instrument's data to default dump file `iListDumpFile` 443 444 self.priceModel = PriceGenerator() # init PriceGenerator object to work with candles data 445 """PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on. 446 447 See also: `LoadHistory()`, `ShowHistoryChart()` and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator 448 """
Main class init.
Parameters
- token: Bearer token for Tinkoff Invest API. It can be set from environment variable
TKS_API_TOKEN. - accountId: string with numeric user account ID in Tinkoff Broker. It can be found in broker's reports.
Also, this variable can be set from environment variable
TKS_ACCOUNT_ID. - useCache: use default cache file with raw data to use instead of
iList. True by default. Cache is auto-update if new day has come. If you don't want to use cache and always updates raw data then setuseCache=False. - defaultCache: path to default cache file.
dump.jsonby default.
Current TKSBrokerAPI version: major.minor, but the build number define at the build-server only.
Latest version: https://pypi.org/project/tksbrokerapi/
String with ticker, e.g. GOOGL. Use alias for USD000UTSTOM simple as USD, EUR_RUB__TOM as EUR etc. More tickers aliases here: TKSEnums.TKS_TICKER_ALIASES.
See also: SearchByTicker(), SearchInstruments().
String with FIGI, e.g. ticker GOOGL has FIGI BBG009S39JX6.
See also: SearchByFIGI(), SearchInstruments().
Depth of Market (DOM) can be >= 1. Default: 1. It used with --price key to showing DOM with current prices for givens ticker or FIGI.
See also: GetCurrentPrices().
Tinkoff REST API server for real trade operations. Default: https://invest-public-api.tinkoff.ru/rest
See also: API method https://tinkoff.github.io/investAPI/#tinkoff-invest-api_1 and SendAPIRequest().
Headers which send in every request to broker server. Please, do not change it! Default: {"Content-Type": "application/json", "accept": "application/json", "Authorization": "Bearer {your_token}"}.
See also: SendAPIRequest().
Full path to the output file where history candles will be saved or updated. Default: None, it mean that returns only Pandas DataFrame.
See also: History().
Full path to the html file where rendered candles chart stored. Default: index.html.
See also: ShowHistoryChart().
Filename where full available to user instruments list will be saved. Default: instruments.md.
See also: ShowInstrumentsInfo().
Filename with all found instruments searched by part of its ticker, FIGI or name. Default: search-results.md.
See also: SearchInstruments().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: GetListOfPrices().
Filename where prices of selected instruments will be saved. Default: prices.md.
See also: ShowInstrumentsInfo(), RequestBondCoupons() and RequestTradingStatus().
Filename where wider Pandas DataFrame with more information about bonds: main info, current prices,
bonds payment calendar, some statistics will be stored. Default: ext-bonds.xlsx.
See also: ExtendBondsData().
Filename where bonds payment calendar will be saved. Default: calendar.md.
Pandas dataframe with only bonds payment calendar also will be stored to default file calendar.xlsx.
See also: CreateBondsCalendar(), ShowBondsCalendar(), ShowInstrumentInfo(), RequestBondCoupons() and ExtendBondsData().
Filename where current portfolio, open trades and orders will be saved. Default: overview.md.
See also: Overview(), RequestPortfolio(), RequestPositions(), RequestPendingOrders() and RequestStopOrders().
Filename where short digest of the portfolio status will be saved. Default: overview-digest.md.
See also: Overview() with parameter details="digest".
Filename where only open positions, without everything else will be saved. Default: overview-positions.md.
See also: Overview() with parameter details="positions".
Filename where open limits and stop orders will be saved. Default: overview-orders.md.
See also: Overview() with parameter details="orders".
Filename where only the analytics section and the distribution of the portfolio by various categories will be saved. Default: overview-analytics.md.
See also: Overview() with parameter details="analytics".
Filename where history of deals and trade statistics will be saved. Default: deals.md.
See also: Deals().
Filename where table of funds available for withdrawal will be saved. Default: limits.md.
See also: OverviewLimits() and RequestLimits().
Filename where all available user's data (accountIds, common user information, margin status and tariff connections limit) will be saved. Default: user-info.md.
See also: OverviewUserInfo(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits().
Filename where simple table with all available user accounts (accountIds) will be saved. Default: accounts.md.
See also: OverviewAccounts(), RequestAccounts().
Filename where raw data about shares, currencies, bonds, etfs and futures will be stored. Default: dump.json.
Pandas dataframe with raw instruments data also will be stored to default file dump.xlsx.
See also: DumpInstruments() and DumpInstrumentsAsXLSX().
Dictionary with raw data about shares, currencies, bonds, etfs and futures from broker server. Auto-updating and saving dump to the iListDumpFile.
See also: Listing(), DumpInstruments().
PriceGenerator object to work with candles data: load, render interact and non-interact charts and so on.
See also: LoadHistory(), ShowHistoryChart() and the PriceGenerator project: https://github.com/Tim55667757/PriceGenerator
472 def SendAPIRequest(self, url: str, reqType: str = "GET", retry: int = 3, pause: int = 5, debug: bool = False) -> dict: 473 """ 474 Send GET or POST request to broker server and receive JSON object. 475 476 self.header: must be defining with dictionary of headers. 477 self.body: if define then used as request body. None by default. 478 self.timeout: global request timeout, 15 seconds by default. 479 :param url: url with REST request. 480 :param reqType: send "GET" or "POST" request. "GET" by default. 481 :param retry: how many times retry after first request if an 5xx server errors occurred. 482 :param pause: sleep time in seconds between retries. 483 :param debug: if `True` then print more debug information, e.g. request and response parameters, headers etc. 484 :return: response JSON (dictionary) from broker. 485 """ 486 if reqType not in ("GET", "POST"): 487 uLogger.error("You can define request type: 'GET' or 'POST'!") 488 raise Exception("Incorrect value") 489 490 if debug: 491 uLogger.debug("Request parameters:") 492 uLogger.debug(" - REST API URL: {}".format(url)) 493 uLogger.debug(" - request type: {}".format(reqType)) 494 uLogger.debug(" - headers: {}".format(str(self.headers).replace(self.token, "*** request token ***"))) 495 uLogger.debug(" - body: {}".format(self.body)) 496 497 # fast hack to avoid all operations with some tickers/FIGI 498 responseJSON = {} 499 oK = True 500 for item in self.exclude: 501 if item in url: 502 if debug: 503 uLogger.warning("Do not execute operations with list of this tickers/FIGI: {}".format(str(self.exclude))) 504 505 oK = False 506 break 507 508 if oK: 509 counter = 0 510 response = None 511 errMsg = "" 512 513 while not response and counter <= retry: 514 if reqType == "GET": 515 response = requests.get(url, headers=self.headers, data=self.body, timeout=self.timeout) 516 517 if reqType == "POST": 518 response = requests.post(url, headers=self.headers, data=self.body, timeout=self.timeout) 519 520 if debug: 521 uLogger.debug("Response:") 522 uLogger.debug(" - status code: {}".format(response.status_code)) 523 uLogger.debug(" - reason: {}".format(response.reason)) 524 uLogger.debug(" - body length: {}".format(len(response.text))) 525 uLogger.debug(" - headers: {}".format(response.headers)) 526 527 # Server returns some headers: 528 # - `x-ratelimit-limit` - shows the settings of the current user limit for this method. 529 # - `x-ratelimit-remaining` - the number of remaining requests of this type per minute. 530 # - `x-ratelimit-reset` - time in seconds before resetting the request counter. 531 # See: https://tinkoff.github.io/investAPI/grpc/#kreya 532 if "x-ratelimit-remaining" in response.headers.keys() and response.headers["x-ratelimit-remaining"] == "0": 533 rateLimitWait = int(response.headers["x-ratelimit-reset"]) 534 uLogger.debug("Rate limit exceeded. Waiting {} sec. for reset rate limit and then repeat again...".format(rateLimitWait)) 535 sleep(rateLimitWait) 536 537 # Error status codes: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 538 if 400 <= response.status_code < 500: 539 msg = "status code: [{}], response body: {}".format(response.status_code, response.text) 540 uLogger.debug(" - not oK, but do not retry for 4xx errors, {}".format(msg)) 541 counter = retry + 1 542 543 if 500 <= response.status_code < 600: 544 errMsg = "status code: [{}], response body: {}".format(response.status_code, response.text) 545 uLogger.debug(" - not oK, {}".format(errMsg)) 546 counter += 1 547 548 if counter <= retry: 549 uLogger.debug("Retry: [{}]. Wait {} sec. and try again...".format(counter, pause)) 550 sleep(pause) 551 552 responseJSON = self._ParseJSON(response.text) 553 554 if errMsg: 555 uLogger.error("Server returns not `oK` status! See: https://tinkoff.github.io/investAPI/errors/") 556 uLogger.error(" - not oK, {}".format(errMsg)) 557 558 return responseJSON
Send GET or POST request to broker server and receive JSON object.
self.header: must be defining with dictionary of headers. self.body: if define then used as request body. None by default. self.timeout: global request timeout, 15 seconds by default.
Parameters
- url: url with REST request.
- reqType: send "GET" or "POST" request. "GET" by default.
- retry: how many times retry after first request if an 5xx server errors occurred.
- pause: sleep time in seconds between retries.
- debug: if
Truethen print more debug information, e.g. request and response parameters, headers etc.
Returns
response JSON (dictionary) from broker.
591 def Listing(self) -> dict: 592 """ 593 Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server. 594 595 :return: Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures. 596 """ 597 uLogger.debug("Requesting all available instruments for current account. Wait, please...") 598 uLogger.debug("CPU usages for parallel requests: [{}]".format(CPU_USAGES)) 599 600 # this parameters insert to requests: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService 601 # iType is type of instrument, it must be one of supported types in TKS_INSTRUMENTS list. 602 iParams = [{"iType": iType} for iType in TKS_INSTRUMENTS] 603 604 poolUpdater = ThreadPool(processes=CPU_USAGES) # create pool for update instruments in parallel mode 605 listing = poolUpdater.map(self._IWrapper, iParams) # execute update operations 606 poolUpdater.close() 607 608 # Dictionary with all broker instruments: shares, currencies, bonds, etfs and futures. 609 # Next in this code: item[0] is "iType" and item[1] is list of available instruments from the result of _IUpdater() method 610 iList = {item[0]: {instrument["ticker"]: instrument for instrument in item[1]} for item in listing} 611 612 # calculate minimum price increment (step) for all instruments and set up instrument's type: 613 for iType in iList.keys(): 614 for ticker in iList[iType]: 615 iList[iType][ticker]["type"] = iType 616 617 if "minPriceIncrement" in iList[iType][ticker].keys(): 618 iList[iType][ticker]["step"] = NanoToFloat( 619 iList[iType][ticker]["minPriceIncrement"]["units"], 620 iList[iType][ticker]["minPriceIncrement"]["nano"], 621 ) 622 623 else: 624 iList[iType][ticker]["step"] = 0 # hack to avoid empty value in some instruments, e.g. futures 625 626 return iList
Gets JSON with raw data about shares, currencies, bonds, etfs and futures from broker server.
Returns
Dictionary with all available broker instruments: currencies, shares, bonds, etfs and futures.
628 def DumpInstrumentsAsXLSX(self, forceUpdate: bool = False) -> None: 629 """ 630 Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics. 631 632 See also: `DumpInstruments()`, `Listing()`. 633 634 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 635 otherwise just saves exist `iList` as XLSX-file (default: `dump.xlsx`) . 636 """ 637 if self.iListDumpFile is None or not self.iListDumpFile: 638 uLogger.error("Output name of dump file must be defined!") 639 raise Exception("Filename required") 640 641 if not self.iList or forceUpdate: 642 self.iList = self.Listing() 643 644 xlsxDumpFile = self.iListDumpFile.replace(".json", ".xlsx") if self.iListDumpFile.endswith(".json") else self.iListDumpFile + ".xlsx" 645 646 # Save as XLSX with separated sheets for every type of instruments: 647 with pd.ExcelWriter( 648 path=xlsxDumpFile, 649 date_format=TKS_DATE_FORMAT, 650 datetime_format=TKS_DATE_TIME_FORMAT, 651 mode="w", 652 ) as writer: 653 for iType in TKS_INSTRUMENTS: 654 df = pd.DataFrame.from_dict(data=self.iList[iType], orient="index") # generate pandas object from self.iList dictionary 655 df = df[sorted(df)] # sorted by column names 656 df = df.applymap( 657 lambda item: NanoToFloat(item["units"], item["nano"]) if isinstance(item, dict) and "units" in item.keys() and "nano" in item.keys() else item, 658 na_action="ignore", 659 ) # converting numbers from nano-type to float in every cell 660 df.to_excel( 661 writer, 662 sheet_name=iType, 663 encoding="UTF-8", 664 freeze_panes=(1, 1), 665 ) # saving as XLSX-file with freeze first row and column as headers 666 667 uLogger.info("XLSX-file for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxDumpFile)))
Creates XLSX-formatted dump file with raw data of instruments to further used by data scientists or stock analytics.
See also: DumpInstruments(), Listing().
Parameters
669 def DumpInstruments(self, forceUpdate: bool = True) -> str: 670 """ 671 Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server 672 using `Listing()` method. If `iListDumpFile` string is not empty then also save information to this file. 673 674 See also: `DumpInstrumentsAsXLSX()`, `Listing()`. 675 676 :param forceUpdate: if `True` then at first updates data with `Listing()` method, 677 otherwise just saves exist `iList` as JSON-file (default: `dump.json`). 678 :return: serialized JSON formatted `str` with full data of instruments, also saved to the `--output` JSON-file. 679 """ 680 if self.iListDumpFile is None or not self.iListDumpFile: 681 uLogger.error("Output name of dump file must be defined!") 682 raise Exception("Filename required") 683 684 if not self.iList or forceUpdate: 685 self.iList = self.Listing() 686 687 jsonDump = json.dumps(self.iList, indent=4, sort_keys=False) # create JSON object as string 688 with open(self.iListDumpFile, mode="w", encoding="UTF-8") as fH: 689 fH.write(jsonDump) 690 691 uLogger.info("New cache of instruments data was created: [{}]".format(os.path.abspath(self.iListDumpFile))) 692 693 return jsonDump
Receives and returns actual raw data about shares, currencies, bonds, etfs and futures from broker server
using Listing() method. If iListDumpFile string is not empty then also save information to this file.
See also: DumpInstrumentsAsXLSX(), Listing().
Parameters
- forceUpdate: if
Truethen at first updates data withListing()method, otherwise just saves existiListas JSON-file (default:dump.json).
Returns
serialized JSON formatted
strwith full data of instruments, also saved to the--outputJSON-file.
695 def ShowInstrumentInfo(self, iJSON: dict, show: bool = True) -> str: 696 """ 697 Show information about one instrument defined by json data and prints it in Markdown format. 698 699 See also: `SearchByTicker()`, `SearchByFIGI()`, `RequestBondCoupons()`, `ExtendBondsData()`, `ShowBondsCalendar()` and `RequestTradingStatus()`. 700 701 :param iJSON: json data of instrument, example: `iJSON = self.iList["Shares"][self.ticker]` 702 :param show: if `True` then also printing information about instrument and its current price. 703 :return: multilines text in Markdown format with information about one instrument. 704 """ 705 splitLine = "| | |\n" 706 infoText = "" 707 708 if iJSON is not None and iJSON and isinstance(iJSON, dict): 709 info = [ 710 "# Main information: ticker [{}], FIGI [{}]\n\n".format(iJSON["ticker"], iJSON["figi"]), 711 "* Actual at: [{}] (UTC)\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 712 "| Parameters | Values |\n", 713 "|-------------------------------------------------------------|--------------------------------------------------------|\n", 714 "| Ticker: | {:<54} |\n".format(iJSON["ticker"]), 715 "| Full name: | {:<54} |\n".format(iJSON["name"]), 716 ] 717 718 if "sector" in iJSON.keys() and iJSON["sector"]: 719 info.append("| Sector: | {:<54} |\n".format(iJSON["sector"])) 720 721 info.append("| Country of instrument: | {:<54} |\n".format("{}{}".format( 722 "({}) ".format(iJSON["countryOfRisk"]) if "countryOfRisk" in iJSON.keys() and iJSON["countryOfRisk"] else "", 723 iJSON["countryOfRiskName"] if "countryOfRiskName" in iJSON.keys() and iJSON["countryOfRiskName"] else "", 724 ))) 725 726 info.extend([ 727 splitLine, 728 "| FIGI (Financial Instrument Global Identifier): | {:<54} |\n".format(iJSON["figi"]), 729 "| Exchange: | {:<54} |\n".format(iJSON["exchange"]), 730 ]) 731 732 if "isin" in iJSON.keys() and iJSON["isin"]: 733 info.append("| ISIN (International Securities Identification Number): | {:<54} |\n".format(iJSON["isin"])) 734 735 if "classCode" in iJSON.keys(): 736 info.append("| Class Code (exchange section where instrument is traded): | {:<54} |\n".format(iJSON["classCode"])) 737 738 info.extend([ 739 splitLine, 740 "| Current broker security trading status: | {:<54} |\n".format(TKS_TRADING_STATUSES[iJSON["tradingStatus"]]), 741 splitLine, 742 "| Buy operations allowed: | {:<54} |\n".format("Yes" if iJSON["buyAvailableFlag"] else "No"), 743 "| Sale operations allowed: | {:<54} |\n".format("Yes" if iJSON["sellAvailableFlag"] else "No"), 744 "| Short positions allowed: | {:<54} |\n".format("Yes" if iJSON["shortEnabledFlag"] else "No"), 745 ]) 746 747 if iJSON["figi"]: 748 self.figi = iJSON["figi"] 749 iJSON = iJSON | self.RequestTradingStatus() 750 751 info.extend([ 752 splitLine, 753 "| Limit orders allowed: | {:<54} |\n".format("Yes" if iJSON["limitOrderAvailableFlag"] else "No"), 754 "| Market orders allowed: | {:<54} |\n".format("Yes" if iJSON["marketOrderAvailableFlag"] else "No"), 755 "| API trade allowed: | {:<54} |\n".format("Yes" if iJSON["apiTradeAvailableFlag"] else "No"), 756 ]) 757 758 info.append(splitLine) 759 760 if "type" in iJSON.keys() and iJSON["type"]: 761 info.append("| Type of the instrument: | {:<54} |\n".format(iJSON["type"])) 762 763 if "futuresType" in iJSON.keys() and iJSON["futuresType"]: 764 info.append("| Futures type: | {:<54} |\n".format(iJSON["futuresType"])) 765 766 if "ipoDate" in iJSON.keys() and iJSON["ipoDate"]: 767 info.append("| IPO date: | {:<54} |\n".format(iJSON["ipoDate"].replace("T", " ").replace("Z", ""))) 768 769 if "releasedDate" in iJSON.keys() and iJSON["releasedDate"]: 770 info.append("| Released date: | {:<54} |\n".format(iJSON["releasedDate"].replace("T", " ").replace("Z", ""))) 771 772 if "rebalancingFreq" in iJSON.keys() and iJSON["rebalancingFreq"]: 773 info.append("| Rebalancing frequency: | {:<54} |\n".format(iJSON["rebalancingFreq"])) 774 775 if "focusType" in iJSON.keys() and iJSON["focusType"]: 776 info.append("| Focusing type: | {:<54} |\n".format(iJSON["focusType"])) 777 778 if "assetType" in iJSON.keys() and iJSON["assetType"]: 779 info.append("| Asset type: | {:<54} |\n".format(iJSON["assetType"])) 780 781 if "basicAsset" in iJSON.keys() and iJSON["basicAsset"]: 782 info.append("| Basic asset: | {:<54} |\n".format(iJSON["basicAsset"])) 783 784 if "basicAssetSize" in iJSON.keys() and iJSON["basicAssetSize"]: 785 info.append("| Basic asset size: | {:<54} |\n".format("{:.2f}".format(NanoToFloat(str(iJSON["basicAssetSize"]["units"]), iJSON["basicAssetSize"]["nano"])))) 786 787 if "isoCurrencyName" in iJSON.keys() and iJSON["isoCurrencyName"]: 788 info.append("| ISO currency name: | {:<54} |\n".format(iJSON["isoCurrencyName"])) 789 790 if "currency" in iJSON.keys(): 791 info.append("| Payment currency: | {:<54} |\n".format(iJSON["currency"])) 792 793 if iJSON["type"] == "Bonds" and "nominal" in iJSON.keys() and "currency" in iJSON["nominal"].keys(): 794 info.append("| Nominal currency: | {:<54} |\n".format(iJSON["nominal"]["currency"])) 795 796 if "firstTradeDate" in iJSON.keys() and iJSON["firstTradeDate"]: 797 info.append("| First trade date: | {:<54} |\n".format(iJSON["firstTradeDate"].replace("T", " ").replace("Z", ""))) 798 799 if "lastTradeDate" in iJSON.keys() and iJSON["lastTradeDate"]: 800 info.append("| Last trade date: | {:<54} |\n".format(iJSON["lastTradeDate"].replace("T", " ").replace("Z", ""))) 801 802 if "expirationDate" in iJSON.keys() and iJSON["expirationDate"]: 803 info.append("| Date of expiration: | {:<54} |\n".format(iJSON["expirationDate"].replace("T", " ").replace("Z", ""))) 804 805 if "stateRegDate" in iJSON.keys() and iJSON["stateRegDate"]: 806 info.append("| State registration date: | {:<54} |\n".format(iJSON["stateRegDate"].replace("T", " ").replace("Z", ""))) 807 808 if "placementDate" in iJSON.keys() and iJSON["placementDate"]: 809 info.append("| Placement date: | {:<54} |\n".format(iJSON["placementDate"].replace("T", " ").replace("Z", ""))) 810 811 if "maturityDate" in iJSON.keys() and iJSON["maturityDate"]: 812 info.append("| Maturity date: | {:<54} |\n".format(iJSON["maturityDate"].replace("T", " ").replace("Z", ""))) 813 814 if "perpetualFlag" in iJSON.keys() and iJSON["perpetualFlag"]: 815 info.append("| Perpetual bond: | Yes |\n") 816 817 if "otcFlag" in iJSON.keys() and iJSON["otcFlag"]: 818 info.append("| Over-the-counter (OTC) securities: | Yes |\n") 819 820 iExt = None 821 if iJSON["type"] == "Bonds": 822 info.extend([ 823 splitLine, 824 "| Bond issue (size / plan): | {:<54} |\n".format("{} / {}".format(iJSON["issueSize"], iJSON["issueSizePlan"])), 825 "| Nominal price (100%): | {:<54} |\n".format("{} {}".format( 826 "{:.2f}".format(NanoToFloat(str(iJSON["nominal"]["units"]), iJSON["nominal"]["nano"])).rstrip("0").rstrip("."), 827 iJSON["nominal"]["currency"], 828 )), 829 ]) 830 831 if "floatingCouponFlag" in iJSON.keys(): 832 info.append("| Floating coupon: | {:<54} |\n".format("Yes" if iJSON["floatingCouponFlag"] else "No")) 833 834 if "amortizationFlag" in iJSON.keys(): 835 info.append("| Amortization: | {:<54} |\n".format("Yes" if iJSON["amortizationFlag"] else "No")) 836 837 info.append(splitLine) 838 839 if "couponQuantityPerYear" in iJSON.keys() and iJSON["couponQuantityPerYear"]: 840 info.append("| Number of coupon payments per year: | {:<54} |\n".format(iJSON["couponQuantityPerYear"])) 841 842 iExt = self.ExtendBondsData(instruments=iJSON["figi"], xlsx=False) # extended bonds data 843 844 info.extend([ 845 "| Days last to maturity date: | {:<54} |\n".format(iExt["daysToMaturity"][0]), 846 "| Coupons yield (average coupon daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["couponsYield"][0])), 847 "| Current price yield (average daily yield * 365): | {:<54} |\n".format("{:.2f}%".format(iExt["currentYield"][0])), 848 ]) 849 850 if "aciValue" in iJSON.keys() and iJSON["aciValue"]: 851 info.append("| Current accumulated coupon income (ACI): | {:<54} |\n".format("{:.2f} {}".format( 852 NanoToFloat(str(iJSON["aciValue"]["units"]), iJSON["aciValue"]["nano"]), 853 iJSON["aciValue"]["currency"] 854 ))) 855 856 if "currentPrice" in iJSON.keys(): 857 info.append(splitLine) 858 859 currency = iJSON["currency"] if "currency" in iJSON.keys() else "" # nominal currency for bonds, otherwise currency of instrument 860 aciCurrency = iExt["aciCurrency"][0] if iJSON["type"] == "Bonds" and iExt is not None and "aciCurrency" in iExt.keys() else "" # payment currency 861 862 bondPrevClose = iExt["closePrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "closePrice" in iExt.keys() else 0 # previous close price of bond 863 bondLastPrice = iExt["lastPrice"][0] if iJSON["type"] == "Bonds" and iExt is not None and "lastPrice" in iExt.keys() else 0 # last price of bond 864 bondLimitUp = iExt["limitUp"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitUp" in iExt.keys() else 0 # max price of bond 865 bondLimitDown = iExt["limitDown"][0] if iJSON["type"] == "Bonds" and iExt is not None and "limitDown" in iExt.keys() else 0 # min price of bond 866 bondChangesDelta = iExt["changesDelta"][0] if iJSON["type"] == "Bonds" and iExt is not None and "changesDelta" in iExt.keys() else 0 # delta between last deal price and last close 867 868 curPriceSell = iJSON["currentPrice"]["sell"][0]["price"] if iJSON["currentPrice"]["sell"] else 0 869 curPriceBuy = iJSON["currentPrice"]["buy"][0]["price"] if iJSON["currentPrice"]["buy"] else 0 870 871 info.extend([ 872 "| Previous close price of the instrument: | {:<54} |\n".format("{}{}".format( 873 "{}".format(iJSON["currentPrice"]["closePrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["closePrice"] is not None else "N/A", 874 "% of nominal price ({:.2f} {})".format(bondPrevClose, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 875 )), 876 "| Last deal price of the instrument: | {:<54} |\n".format("{}{}".format( 877 "{}".format(iJSON["currentPrice"]["lastPrice"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["lastPrice"] is not None else "N/A", 878 "% of nominal price ({:.2f} {})".format(bondLastPrice, aciCurrency) if iJSON["type"] == "Bonds" else " {}".format(currency), 879 )), 880 "| Changes between last deal price and last close | {:<54} |\n".format( 881 "{:.2f}%{}".format( 882 iJSON["currentPrice"]["changes"], 883 " ({}{:.2f} {})".format( 884 "+" if bondChangesDelta > 0 else "", 885 bondChangesDelta, 886 aciCurrency 887 ) if iJSON["type"] == "Bonds" else " ({}{:.2f} {})".format( 888 "+" if iJSON["currentPrice"]["lastPrice"] > iJSON["currentPrice"]["closePrice"] else "", 889 iJSON["currentPrice"]["lastPrice"] - iJSON["currentPrice"]["closePrice"], 890 currency 891 ), 892 ) 893 ), 894 "| Current limit price, min / max: | {:<54} |\n".format("{}{} / {}{}{}".format( 895 "{}".format(iJSON["currentPrice"]["limitDown"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitDown"] is not None else "N/A", 896 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 897 "{}".format(iJSON["currentPrice"]["limitUp"]).rstrip("0").rstrip(".") if iJSON["currentPrice"]["limitUp"] is not None else "N/A", 898 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 899 " ({:.2f} {} / {:.2f} {})".format(bondLimitDown, aciCurrency, bondLimitUp, aciCurrency) if iJSON["type"] == "Bonds" else "" 900 )), 901 "| Actual price, sell / buy: | {:<54} |\n".format("{}{} / {}{}{}".format( 902 "{}".format(curPriceSell).rstrip("0").rstrip(".") if curPriceSell != 0 else "N/A", 903 "%" if iJSON["type"] == "Bonds" else " {}".format(currency), 904 "{}".format(curPriceBuy).rstrip("0").rstrip(".") if curPriceBuy != 0 else "N/A", 905 "%" if iJSON["type"] == "Bonds" else" {}".format(currency), 906 " ({:.2f} {} / {:.2f} {})".format(curPriceSell, aciCurrency, curPriceBuy, aciCurrency) if iJSON["type"] == "Bonds" else "" 907 )), 908 ]) 909 910 if "lot" in iJSON.keys(): 911 info.append("| Minimum lot to buy: | {:<54} |\n".format(iJSON["lot"])) 912 913 if "step" in iJSON.keys() and iJSON["step"] != 0: 914 info.append("| Minimum price increment (step): | {:<54} |\n".format(iJSON["step"])) 915 916 # Add bond payment calendar: 917 if iJSON["type"] == "Bonds": 918 strCalendar = self.ShowBondsCalendar(extBonds=iExt, show=False) # bond payment calendar 919 info.extend(["\n", strCalendar]) 920 921 infoText += "".join(info) 922 923 if show: 924 uLogger.info("{}".format(infoText)) 925 926 else: 927 uLogger.debug("{}".format(infoText)) 928 929 if self.infoFile is not None: 930 with open(self.infoFile, "w", encoding="UTF-8") as fH: 931 fH.write(infoText) 932 933 uLogger.info("Info about instrument with ticker [{}] and FIGI [{}] was saved to file: [{}]".format(iJSON["ticker"], iJSON["figi"], os.path.abspath(self.infoFile))) 934 935 return infoText
Show information about one instrument defined by json data and prints it in Markdown format.
See also: SearchByTicker(), SearchByFIGI(), RequestBondCoupons(), ExtendBondsData(), ShowBondsCalendar() and RequestTradingStatus().
Parameters
- iJSON: json data of instrument, example:
iJSON = self.iList["Shares"][self.ticker] - show: if
Truethen also printing information about instrument and its current price.
Returns
multilines text in Markdown format with information about one instrument.
937 def SearchByTicker(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 938 """ 939 Search and return raw broker's information about instrument by its ticker. 940 `ticker` must be defined! If debug=True then print all debug messages. 941 942 :param requestPrice: if `False` then do not request current price of instrument (because this is long operation). 943 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 944 :param debug: if `True` then print all debug console messages. 945 :return: JSON formatted data with information about instrument. 946 """ 947 tickerJSON = {} 948 if debug: 949 uLogger.debug("Searching information about instrument by it's ticker [{}] ...".format(self.ticker)) 950 951 if not self.ticker: 952 uLogger.warning("self.ticker variable is not be empty!") 953 954 else: 955 if self.ticker in TKS_TICKERS_OR_FIGI_EXCLUDED: 956 uLogger.warning("Instrument with ticker [{}] not allowed for trading!".format(self.ticker)) 957 raise Exception("Instrument not allowed") 958 959 if not self.iList: 960 self.iList = self.Listing() 961 962 if self.ticker in self.iList["Shares"].keys(): 963 tickerJSON = self.iList["Shares"][self.ticker] 964 if debug: 965 uLogger.debug("Ticker [{}] found in shares list".format(self.ticker)) 966 967 elif self.ticker in self.iList["Currencies"].keys(): 968 tickerJSON = self.iList["Currencies"][self.ticker] 969 if debug: 970 uLogger.debug("Ticker [{}] found in currencies list".format(self.ticker)) 971 972 elif self.ticker in self.iList["Bonds"].keys(): 973 tickerJSON = self.iList["Bonds"][self.ticker] 974 if debug: 975 uLogger.debug("Ticker [{}] found in bonds list".format(self.ticker)) 976 977 elif self.ticker in self.iList["Etfs"].keys(): 978 tickerJSON = self.iList["Etfs"][self.ticker] 979 if debug: 980 uLogger.debug("Ticker [{}] found in etfs list".format(self.ticker)) 981 982 elif self.ticker in self.iList["Futures"].keys(): 983 tickerJSON = self.iList["Futures"][self.ticker] 984 if debug: 985 uLogger.debug("Ticker [{}] found in futures list".format(self.ticker)) 986 987 if tickerJSON: 988 self.figi = tickerJSON["figi"] 989 990 if requestPrice: 991 tickerJSON["currentPrice"] = self.GetCurrentPrices(show=False) 992 993 if tickerJSON["currentPrice"]["closePrice"] is not None and tickerJSON["currentPrice"]["closePrice"] != 0 and tickerJSON["currentPrice"]["lastPrice"] is not None: 994 tickerJSON["currentPrice"]["changes"] = 100 * (tickerJSON["currentPrice"]["lastPrice"] - tickerJSON["currentPrice"]["closePrice"]) / tickerJSON["currentPrice"]["closePrice"] 995 996 else: 997 tickerJSON["currentPrice"]["changes"] = 0 998 999 if show: 1000 self.ShowInstrumentInfo(iJSON=tickerJSON, show=True) # print info as Markdown text 1001 1002 else: 1003 if show: 1004 uLogger.warning("Ticker [{}] not found in available broker instrument's list!".format(self.ticker)) 1005 1006 return tickerJSON
Search and return raw broker's information about instrument by its ticker.
ticker must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (because this is long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1008 def SearchByFIGI(self, requestPrice: bool = False, show: bool = False, debug: bool = False) -> dict: 1009 """ 1010 Search and return raw broker's information about instrument by its FIGI. 1011 `figi` must be defined! If debug=True then print all debug messages. 1012 1013 :param requestPrice: if `False` then do not request current price of instrument (it's long operation). 1014 :param show: if `False` then do not run `ShowInstrumentInfo()` method and do not print info to the console. 1015 :param debug: if `True` then print all debug console messages. 1016 :return: JSON formatted data with information about instrument. 1017 """ 1018 figiJSON = {} 1019 if debug: 1020 uLogger.debug("Searching information about instrument by it's FIGI [{}] ...".format(self.figi)) 1021 1022 if not self.figi: 1023 uLogger.warning("self.figi variable is not be empty!") 1024 1025 else: 1026 if self.figi in TKS_TICKERS_OR_FIGI_EXCLUDED: 1027 uLogger.warning("Instrument with figi [{}] not allowed for trading!".format(self.figi)) 1028 raise Exception("Instrument not allowed") 1029 1030 if not self.iList: 1031 self.iList = self.Listing() 1032 1033 for item in self.iList["Shares"].keys(): 1034 if self.figi == self.iList["Shares"][item]["figi"]: 1035 figiJSON = self.iList["Shares"][item] 1036 1037 if debug: 1038 uLogger.debug("FIGI [{}] found in shares list".format(self.figi)) 1039 1040 break 1041 1042 if not figiJSON: 1043 for item in self.iList["Currencies"].keys(): 1044 if self.figi == self.iList["Currencies"][item]["figi"]: 1045 figiJSON = self.iList["Currencies"][item] 1046 1047 if debug: 1048 uLogger.debug("FIGI [{}] found in currencies list".format(self.figi)) 1049 1050 break 1051 1052 if not figiJSON: 1053 for item in self.iList["Bonds"].keys(): 1054 if self.figi == self.iList["Bonds"][item]["figi"]: 1055 figiJSON = self.iList["Bonds"][item] 1056 1057 if debug: 1058 uLogger.debug("FIGI [{}] found in bonds list".format(self.figi)) 1059 1060 break 1061 1062 if not figiJSON: 1063 for item in self.iList["Etfs"].keys(): 1064 if self.figi == self.iList["Etfs"][item]["figi"]: 1065 figiJSON = self.iList["Etfs"][item] 1066 1067 if debug: 1068 uLogger.debug("FIGI [{}] found in etfs list".format(self.figi)) 1069 1070 break 1071 1072 if not figiJSON: 1073 for item in self.iList["Futures"].keys(): 1074 if self.figi == self.iList["Futures"][item]["figi"]: 1075 figiJSON = self.iList["Futures"][item] 1076 1077 if debug: 1078 uLogger.debug("FIGI [{}] found in futures list".format(self.figi)) 1079 1080 break 1081 1082 if figiJSON: 1083 self.figi = figiJSON["figi"] 1084 self.ticker = figiJSON["ticker"] 1085 1086 if requestPrice: 1087 figiJSON["currentPrice"] = self.GetCurrentPrices(show=False) 1088 1089 if figiJSON["currentPrice"]["closePrice"] is not None and figiJSON["currentPrice"]["closePrice"] != 0 and figiJSON["currentPrice"]["lastPrice"] is not None: 1090 figiJSON["currentPrice"]["changes"] = 100 * (figiJSON["currentPrice"]["lastPrice"] - figiJSON["currentPrice"]["closePrice"]) / figiJSON["currentPrice"]["closePrice"] 1091 1092 else: 1093 figiJSON["currentPrice"]["changes"] = 0 1094 1095 if show: 1096 self.ShowInstrumentInfo(iJSON=figiJSON, show=True) # print info as Markdown text 1097 1098 else: 1099 if show: 1100 uLogger.warning("FIGI [{}] not found in available broker instrument's list!".format(self.figi)) 1101 1102 return figiJSON
Search and return raw broker's information about instrument by its FIGI.
figi must be defined! If debug=True then print all debug messages.
Parameters
- requestPrice: if
Falsethen do not request current price of instrument (it's long operation). - show: if
Falsethen do not runShowInstrumentInfo()method and do not print info to the console. - debug: if
Truethen print all debug console messages.
Returns
JSON formatted data with information about instrument.
1104 def GetCurrentPrices(self, show: bool = True) -> dict: 1105 """ 1106 Get and show Depth of Market with current prices of the instrument as dictionary. Result example with `depth` 5: 1107 `{"buy": [{"price": 1243.8, "quantity": 193}, 1108 {"price": 1244.0, "quantity": 168}, 1109 {"price": 1244.8, "quantity": 5}, 1110 {"price": 1245.0, "quantity": 61}, 1111 {"price": 1245.4, "quantity": 60}], 1112 "sell": [{"price": 1243.6, "quantity": 8}, 1113 {"price": 1242.6, "quantity": 10}, 1114 {"price": 1242.4, "quantity": 18}, 1115 {"price": 1242.2, "quantity": 50}, 1116 {"price": 1242.0, "quantity": 113}], 1117 "limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}`, where parameters mean: 1118 - buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order 1119 - sell: list of dicts with Buyers prices, 1120 - price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument), 1121 - quantity: volume value by current price in lots, 1122 - limitUp: current trade session limit price, maximum, 1123 - limitDown: current trade session limit price, minimum, 1124 - lastPrice: last deal price of the instrument, 1125 - closePrice: previous trade session close price of the instrument. 1126 1127 See also: `SearchByTicker()` and `SearchByFIGI()`. 1128 REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1129 Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1130 1131 :param show: if `True` then print DOM to log and console. 1132 :return: orders book dict with lists of current buy and sell prices: `{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}`. 1133 If an error occurred then returns an empty record: 1134 `{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}`. 1135 """ 1136 prices = {"buy": [], "sell": [], "limitUp": 0, "limitDown": 0, "lastPrice": 0, "closePrice": 0} 1137 1138 if self.depth < 1: 1139 uLogger.error("Depth of Market (DOM) must be >=1!") 1140 raise Exception("Incorrect value") 1141 1142 if not (self.ticker or self.figi): 1143 uLogger.error("self.ticker or self.figi variables must be defined!") 1144 raise Exception("Ticker or FIGI required") 1145 1146 if self.ticker and not self.figi: 1147 instrumentByTicker = self.SearchByTicker(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1148 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 1149 1150 if not self.ticker and self.figi: 1151 instrumentByFigi = self.SearchByFIGI(requestPrice=False) # WARNING! requestPrice=False to avoid recursion! 1152 self.ticker = instrumentByFigi["ticker"] if instrumentByFigi else "" 1153 1154 if not self.figi: 1155 uLogger.error("FIGI is not defined!") 1156 raise Exception("Ticker or FIGI required") 1157 1158 else: 1159 uLogger.debug("Requesting current prices: ticker [{}], FIGI [{}]. Wait, please...".format(self.ticker, self.figi)) 1160 1161 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook 1162 priceURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetOrderBook" 1163 self.body = str({"figi": self.figi, "depth": self.depth}) 1164 pricesResponse = self.SendAPIRequest(priceURL, reqType="POST") # Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse 1165 1166 if pricesResponse: 1167 # list of dicts with sellers orders: 1168 prices["buy"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["asks"]] 1169 1170 # list of dicts with buyers orders: 1171 prices["sell"] = [{"price": round(NanoToFloat(item["price"]["units"], item["price"]["nano"]), 6), "quantity": int(item["quantity"])} for item in pricesResponse["bids"]] 1172 1173 # max price of instrument at this time: 1174 prices["limitUp"] = round(NanoToFloat(pricesResponse["limitUp"]["units"], pricesResponse["limitUp"]["nano"]), 6) if "limitUp" in pricesResponse.keys() else None 1175 1176 # min price of instrument at this time: 1177 prices["limitDown"] = round(NanoToFloat(pricesResponse["limitDown"]["units"], pricesResponse["limitDown"]["nano"]), 6) if "limitDown" in pricesResponse.keys() else None 1178 1179 # last price of deal with instrument: 1180 prices["lastPrice"] = round(NanoToFloat(pricesResponse["lastPrice"]["units"], pricesResponse["lastPrice"]["nano"]), 6) if "lastPrice" in pricesResponse.keys() else 0 1181 1182 # last close price of instrument: 1183 prices["closePrice"] = round(NanoToFloat(pricesResponse["closePrice"]["units"], pricesResponse["closePrice"]["nano"]), 6) if "closePrice" in pricesResponse.keys() else 0 1184 1185 else: 1186 uLogger.warning("Server return an empty or error response! See full log. Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1187 uLogger.debug("Server response: {}".format(pricesResponse)) 1188 1189 if show: 1190 if prices["buy"] or prices["sell"]: 1191 info = [ 1192 "Orders book actual at [{}] (UTC)\nTicker: [{}], FIGI: [{}], Depth of Market: [{}]\n".format( 1193 datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 1194 self.ticker, 1195 self.figi, 1196 self.depth, 1197 ), 1198 "-" * 60, "\n", 1199 " Orders of Buyers | Orders of Sellers\n", 1200 "-" * 60, "\n", 1201 " Sell prices (volumes) | Buy prices (volumes)\n", 1202 "-" * 60, "\n", 1203 ] 1204 1205 if not prices["buy"]: 1206 info.append(" | No orders!\n") 1207 sumBuy = 0 1208 1209 else: 1210 sumBuy = sum([x["quantity"] for x in prices["buy"]]) 1211 maxMinSorted = sorted(prices["buy"], key=lambda k: k["price"], reverse=True) 1212 for item in maxMinSorted: 1213 info.append(" | {} ({})\n".format(item["price"], item["quantity"])) 1214 1215 if not prices["sell"]: 1216 info.append("No orders! |\n") 1217 sumSell = 0 1218 1219 else: 1220 sumSell = sum([x["quantity"] for x in prices["sell"]]) 1221 for item in prices["sell"]: 1222 info.append("{:>29} |\n".format("{} ({})".format(item["price"], item["quantity"]))) 1223 1224 info.extend([ 1225 "-" * 60, "\n", 1226 "{:>29} | {}\n".format("Total sell: {}".format(sumSell), "Total buy: {}".format(sumBuy)), 1227 "-" * 60, "\n", 1228 ]) 1229 1230 infoText = "".join(info) 1231 1232 uLogger.info("Current prices in order book:\n\n{}".format(infoText)) 1233 1234 else: 1235 uLogger.warning("Orders book is empty at this time! Instrument: ticker [{}], FIGI [{}]".format(self.ticker, self.figi)) 1236 1237 return prices
Get and show Depth of Market with current prices of the instrument as dictionary. Result example with depth 5:
{"buy": [{"price": 1243.8, "quantity": 193},
{"price": 1244.0, "quantity": 168},
{"price": 1244.8, "quantity": 5},
{"price": 1245.0, "quantity": 61},
{"price": 1245.4, "quantity": 60}],
"sell": [{"price": 1243.6, "quantity": 8},
{"price": 1242.6, "quantity": 10},
{"price": 1242.4, "quantity": 18},
{"price": 1242.2, "quantity": 50},
{"price": 1242.0, "quantity": 113}],
"limitUp": 1619.0, "limitDown": 903.4, "lastPrice": 1243.8, "closePrice": 1263.0}, where parameters mean:
- buy: list of dicts with Sellers prices, see also: https://tinkoff.github.io/investAPI/marketdata/#order
- sell: list of dicts with Buyers prices,
- price: price of 1 instrument (to get the cost of the lot, you need to multiply it by the lot of size of the instrument),
- quantity: volume value by current price in lots,
- limitUp: current trade session limit price, maximum,
- limitDown: current trade session limit price, minimum,
- lastPrice: last deal price of the instrument,
- closePrice: previous trade session close price of the instrument.
See also: SearchByTicker() and SearchByFIGI().
REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetOrderBook
Response fields: https://tinkoff.github.io/investAPI/marketdata/#getorderbookresponse
Parameters
- show: if
Truethen print DOM to log and console.
Returns
orders book dict with lists of current buy and sell prices:
{"buy": [{"price": x1, "quantity": y1, ...}], "sell": [....]}. If an error occurred then returns an empty record:{"buy": [], "sell": [], "limitUp": None, "limitDown": None, "lastPrice": None, "closePrice": None}.
1239 def ShowInstrumentsInfo(self, show: bool = True) -> str: 1240 """ 1241 This method get and show information about all available broker instruments for current user account. 1242 If `instrumentsFile` string is not empty then also save information to this file. 1243 1244 :param show: if `True` then print results to console, if `False` - print only to file. 1245 :return: multi-lines string with all available broker instruments 1246 """ 1247 if not self.iList: 1248 self.iList = self.Listing() 1249 1250 info = [ 1251 "# All available instruments from Tinkoff Broker server for current user token\n\n", 1252 "* **Actual on date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1253 ] 1254 1255 # add instruments count by type: 1256 for iType in self.iList.keys(): 1257 info.append("* **{}:** [{}]\n".format(iType, len(self.iList[iType]))) 1258 1259 headerLine = "| Ticker | Full name | FIGI | Cur | Lot | Step |\n" 1260 splitLine = "|--------------|-----------------------------------------------------------|--------------|-----|---------|------------|\n" 1261 1262 # generating info tables with all instruments by type: 1263 for iType in self.iList.keys(): 1264 info.extend(["\n\n## {} available. Total: [{}]\n\n".format(iType, len(self.iList[iType])), headerLine, splitLine]) 1265 1266 for instrument in self.iList[iType].keys(): 1267 iName = self.iList[iType][instrument]["name"] # instrument's name 1268 if len(iName) > 57: 1269 iName = "{}...".format(iName[:54]) # right trim for a long string 1270 1271 info.append("| {:<12} | {:<57} | {:<12} | {:<3} | {:<7} | {:<10} |\n".format( 1272 self.iList[iType][instrument]["ticker"], 1273 iName, 1274 self.iList[iType][instrument]["figi"], 1275 self.iList[iType][instrument]["currency"], 1276 self.iList[iType][instrument]["lot"], 1277 "{:.10f}".format(self.iList[iType][instrument]["step"]).rstrip("0").rstrip(".") if self.iList[iType][instrument]["step"] > 0 else 0, 1278 )) 1279 1280 infoText = "".join(info) 1281 1282 if show: 1283 uLogger.info(infoText) 1284 1285 if self.instrumentsFile: 1286 with open(self.instrumentsFile, "w", encoding="UTF-8") as fH: 1287 fH.write(infoText) 1288 1289 uLogger.info("All available instruments are saved to file: [{}]".format(os.path.abspath(self.instrumentsFile))) 1290 1291 return infoText
This method get and show information about all available broker instruments for current user account.
If instrumentsFile string is not empty then also save information to this file.
Parameters
- show: if
Truethen print results to console, ifFalse- print only to file.
Returns
multi-lines string with all available broker instruments
1293 def SearchInstruments(self, pattern: str, show: bool = True) -> dict: 1294 """ 1295 This method search and show information about instruments by part of its ticker, FIGI or name. 1296 If `searchResultsFile` string is not empty then also save information to this file. 1297 1298 :param pattern: string with part of ticker, FIGI or instrument's name. 1299 :param show: if `True` then print results to console, if `False` - return list of result only. 1300 :return: list of dictionaries with all found instruments. 1301 """ 1302 if not self.iList: 1303 self.iList = self.Listing() 1304 1305 searchResults = {iType: {} for iType in self.iList} # same as iList but will contains only filtered instruments 1306 compiledPattern = re.compile(pattern, re.IGNORECASE) 1307 1308 for iType in self.iList: 1309 for instrument in self.iList[iType].values(): 1310 searchResult = compiledPattern.search(" ".join( 1311 [instrument["ticker"], instrument["figi"], instrument["name"]] 1312 )) 1313 1314 if searchResult: 1315 searchResults[iType][instrument["ticker"]] = instrument 1316 1317 resultsLen = sum([len(searchResults[iType]) for iType in searchResults]) 1318 info = [ 1319 "# Search results\n\n", 1320 "* **Search pattern:** [{}]\n".format(pattern), 1321 "* **Found instruments:** [{}]\n\n".format(resultsLen), 1322 "**Note:** you can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t TICKER --info` or `tksbrokerapi -f FIGI --info`.\n" 1323 ] 1324 infoShort = info[:] 1325 1326 headerLine = "| Type | Ticker | Full name | FIGI |\n" 1327 splitLine = "|------------|--------------|----------------------------------------------------------------|--------------|\n" 1328 skippedLine = "| ... | ... | ... | ... |\n" 1329 1330 if resultsLen == 0: 1331 info.append("\nNo results\n") 1332 infoShort.append("\nNo results\n") 1333 uLogger.warning("No results. Try changing your search pattern.") 1334 1335 else: 1336 for iType in searchResults: 1337 iTypeValuesCount = len(searchResults[iType].values()) 1338 if iTypeValuesCount > 0: 1339 info.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1340 infoShort.extend(["\n### {}: [{}]\n\n".format(iType, iTypeValuesCount), headerLine, splitLine]) 1341 1342 for instrument in searchResults[iType].values(): 1343 info.append("| {:<10} | {:<12} | {:<63}| {:<13}|\n".format( 1344 instrument["type"], 1345 instrument["ticker"], 1346 "{}...".format(instrument["name"][:60]) if len(instrument["name"]) > 63 else instrument["name"], # right trim for a long string 1347 instrument["figi"], 1348 )) 1349 1350 if iTypeValuesCount <= 5: 1351 infoShort.extend(info[-iTypeValuesCount:]) 1352 1353 else: 1354 infoShort.extend(info[-5:]) 1355 infoShort.append(skippedLine) 1356 1357 infoText = "".join(info) 1358 infoTextShort = "".join(infoShort) 1359 1360 if show: 1361 uLogger.info(infoTextShort) 1362 uLogger.info("You can view info about found instruments with key `--info`, e.g.: `tksbrokerapi -t IBM --info` or `tksbrokerapi -f BBG000BLNNH6 --info`") 1363 1364 if self.searchResultsFile: 1365 with open(self.searchResultsFile, "w", encoding="UTF-8") as fH: 1366 fH.write(infoText) 1367 1368 uLogger.info("Full search results were saved to file: [{}]".format(os.path.abspath(self.searchResultsFile))) 1369 1370 return searchResults
This method search and show information about instruments by part of its ticker, FIGI or name.
If searchResultsFile string is not empty then also save information to this file.
Parameters
- pattern: string with part of ticker, FIGI or instrument's name.
- show: if
Truethen print results to console, ifFalse- return list of result only.
Returns
list of dictionaries with all found instruments.
1372 def GetUniqueFIGIs(self, instruments: list[str]) -> list: 1373 """ 1374 Creating list with unique instrument FIGIs from input list of tickers or FIGIs. 1375 1376 :param instruments: list of strings with tickers or FIGIs. 1377 :return: list with unique instrument FIGIs only. 1378 """ 1379 requestedInstruments = [] 1380 for iName in instruments: 1381 if iName not in self.aliases.keys(): 1382 if iName not in requestedInstruments: 1383 requestedInstruments.append(iName) 1384 1385 else: 1386 if iName not in requestedInstruments: 1387 if self.aliases[iName] not in requestedInstruments: 1388 requestedInstruments.append(self.aliases[iName]) 1389 1390 uLogger.debug("Requested instruments without duplicates of tickers or FIGIs: {}".format(requestedInstruments)) 1391 1392 onlyUniqueFIGIs = [] 1393 for iName in requestedInstruments: 1394 if iName in TKS_TICKERS_OR_FIGI_EXCLUDED: 1395 continue 1396 1397 self.ticker = iName 1398 iData = self.SearchByTicker(requestPrice=False) # trying to find instrument by ticker 1399 1400 if not iData: 1401 self.ticker = "" 1402 self.figi = iName 1403 1404 iData = self.SearchByFIGI(requestPrice=False) # trying to find instrument by FIGI 1405 1406 if not iData: 1407 self.figi = "" 1408 uLogger.warning("Instrument [{}] not in list of available instruments for current token!".format(iName)) 1409 1410 if iData and iData["figi"] not in onlyUniqueFIGIs: 1411 onlyUniqueFIGIs.append(iData["figi"]) 1412 1413 uLogger.debug("Unique list of FIGIs: {}".format(onlyUniqueFIGIs)) 1414 1415 return onlyUniqueFIGIs
Creating list with unique instrument FIGIs from input list of tickers or FIGIs.
Parameters
- instruments: list of strings with tickers or FIGIs.
Returns
list with unique instrument FIGIs only.
1417 def GetListOfPrices(self, instruments: list, show: bool = False) -> list: 1418 """ 1419 This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation! 1420 See limits: https://tinkoff.github.io/investAPI/limits/ 1421 If `pricesFile` string is not empty then also save information to this file. 1422 1423 :param instruments: list of strings with tickers or FIGIs. 1424 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1425 :return: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1426 One item is dict returned by `SearchByTicker()` or `SearchByFIGI()` methods. 1427 """ 1428 if instruments is None or not instruments: 1429 uLogger.error("You must define some of tickers or FIGIs to request it's actual prices!") 1430 raise Exception("Ticker or FIGI required") 1431 1432 onlyUniqueFIGIs = self.GetUniqueFIGIs(instruments) 1433 1434 uLogger.debug("Requesting current prices from Tinkoff Broker server...") 1435 1436 iList = [] # trying to get info and current prices about all unique instruments: 1437 for self.figi in onlyUniqueFIGIs: 1438 iData = self.SearchByFIGI(requestPrice=True) 1439 iList.append(iData) 1440 1441 self.ShowListOfPrices(iList, show) 1442 1443 return iList
This method get, maybe show and return prices of list of instruments. WARNING! This is potential long operation!
See limits: https://tinkoff.github.io/investAPI/limits/
If pricesFile string is not empty then also save information to this file.
Parameters
- instruments: list of strings with tickers or FIGIs.
- show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
list of instruments looks like
[{some ticker info, "currentPrice": {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker()orSearchByFIGI()methods.
1445 def ShowListOfPrices(self, iList: list, show: bool = True) -> str: 1446 """ 1447 Show table contains current prices of given instruments. 1448 1449 :param iList: list of instruments looks like `[{some ticker info, "currentPrice": {current prices}}, {...}, ...]`. 1450 One item is dict returned by `SearchByTicker(requestPrice=True)` or by `SearchByFIGI(requestPrice=True)` methods. 1451 :param show: if `True` then prints prices to console, if `False` - prints only to file `pricesFile`. 1452 :return: multilines text in Markdown format as a table contains current prices. 1453 """ 1454 infoText = "" 1455 1456 if show or self.pricesFile: 1457 info = [ 1458 "# Actual prices at: [{} UTC]\n\n".format(datetime.now(tzutc()).strftime("%Y-%m-%d %H:%M")), 1459 "| Ticker | FIGI | Type | Prev. close | Last price | Chg. % | Day limits min/max | Actual sell / buy | Curr. |\n", 1460 "|--------------|--------------|------------|-------------|-------------|----------|---------------------|---------------------|-------|\n", 1461 ] 1462 1463 for item in iList: 1464 info.append("| {:<12} | {:<12} | {:<10} | {:>11} | {:>11} | {:>7}% | {:>19} | {:>19} | {:<5} |\n".format( 1465 item["ticker"], 1466 item["figi"], 1467 item["type"], 1468 "{:.2f}".format(float(item["currentPrice"]["closePrice"])), 1469 "{:.2f}".format(float(item["currentPrice"]["lastPrice"])), 1470 "{}{:.2f}".format("+" if item["currentPrice"]["changes"] > 0 else "", float(item["currentPrice"]["changes"])), 1471 "{} / {}".format( 1472 item["currentPrice"]["limitDown"] if item["currentPrice"]["limitDown"] is not None else "N/A", 1473 item["currentPrice"]["limitUp"] if item["currentPrice"]["limitUp"] is not None else "N/A", 1474 ), 1475 "{} / {}".format( 1476 item["currentPrice"]["sell"][0]["price"] if item["currentPrice"]["sell"] else "N/A", 1477 item["currentPrice"]["buy"][0]["price"] if item["currentPrice"]["buy"] else "N/A", 1478 ), 1479 item["currency"], 1480 )) 1481 1482 infoText = "".join(info) 1483 1484 if show: 1485 uLogger.info("Only instruments with unique FIGIs are shown:\n{}".format(infoText)) 1486 1487 if self.pricesFile: 1488 with open(self.pricesFile, "w", encoding="UTF-8") as fH: 1489 fH.write(infoText) 1490 1491 uLogger.info("Price list for all instruments saved to file: [{}]".format(os.path.abspath(self.pricesFile))) 1492 1493 return infoText
Show table contains current prices of given instruments.
Parameters
- **iList: list of instruments looks like
[{some ticker info, "currentPrice"**: {current prices}}, {...}, ...]. One item is dict returned bySearchByTicker(requestPrice=True)or bySearchByFIGI(requestPrice=True)methods. - show: if
Truethen prints prices to console, ifFalse- prints only to filepricesFile.
Returns
multilines text in Markdown format as a table contains current prices.
1495 def RequestTradingStatus(self) -> dict: 1496 """ 1497 Requesting trading status for the instrument defined by `figi` variable. 1498 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus 1499 Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest 1500 1501 :return: dictionary with trading status attributes. Response example: 1502 `{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", 1503 "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}` 1504 """ 1505 if self.figi is None or not self.figi: 1506 uLogger.error("Variable `figi` must be defined for using this method!") 1507 raise Exception("FIGI required") 1508 1509 uLogger.debug("Requesting current trading status, FIGI: [{}]. Wait, please...".format(self.figi)) 1510 1511 self.body = str({"figi": self.figi, "instrumentId": self.figi}) 1512 tradingStatusURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetTradingStatus" 1513 tradingStatus = self.SendAPIRequest(tradingStatusURL, reqType="POST") 1514 1515 uLogger.debug("Records about current trading status successfully received") 1516 1517 return tradingStatus
Requesting trading status for the instrument defined by figi variable.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetTradingStatus
Documentation: https://tinkoff.github.io/investAPI/marketdata/#gettradingstatusrequest
Returns
dictionary with trading status attributes. Response example:
{"figi": "TCS00A103X66", "tradingStatus": "SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING", "limitOrderAvailableFlag": false, "marketOrderAvailableFlag": false, "apiTradeAvailableFlag": true}
1519 def RequestPortfolio(self) -> dict: 1520 """ 1521 Requesting actual user's portfolio for current `accountId`. 1522 REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio 1523 Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest 1524 1525 :return: dictionary with user's portfolio. 1526 """ 1527 if self.accountId is None or not self.accountId: 1528 uLogger.error("Variable `accountId` must be defined for using this method!") 1529 raise Exception("Account ID required") 1530 1531 uLogger.debug("Requesting current actual user's portfolio. Wait, please...") 1532 1533 self.body = str({"accountId": self.accountId}) 1534 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPortfolio" 1535 rawPortfolio = self.SendAPIRequest(portfolioURL, reqType="POST") 1536 1537 uLogger.debug("Records about user's portfolio successfully received") 1538 1539 return rawPortfolio
Requesting actual user's portfolio for current accountId.
REST API for user portfolio: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPortfolio
Documentation: https://tinkoff.github.io/investAPI/operations/#portfoliorequest
Returns
dictionary with user's portfolio.
1541 def RequestPositions(self) -> dict: 1542 """ 1543 Requesting open positions by currencies and instruments for current `accountId`. 1544 REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions 1545 Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest 1546 1547 :return: dictionary with open positions by instruments. 1548 """ 1549 if self.accountId is None or not self.accountId: 1550 uLogger.error("Variable `accountId` must be defined for using this method!") 1551 raise Exception("Account ID required") 1552 1553 uLogger.debug("Requesting current open positions in currencies and instruments. Wait, please...") 1554 1555 self.body = str({"accountId": self.accountId}) 1556 positionsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetPositions" 1557 rawPositions = self.SendAPIRequest(positionsURL, reqType="POST") 1558 1559 uLogger.debug("Records about current open positions successfully received") 1560 1561 return rawPositions
Requesting open positions by currencies and instruments for current accountId.
REST API for open positions: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetPositions
Documentation: https://tinkoff.github.io/investAPI/operations/#positionsrequest
Returns
dictionary with open positions by instruments.
1563 def RequestPendingOrders(self) -> list: 1564 """ 1565 Requesting current actual pending orders for current `accountId`. 1566 REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders 1567 Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest 1568 1569 :return: list of dictionaries with pending orders. 1570 """ 1571 if self.accountId is None or not self.accountId: 1572 uLogger.error("Variable `accountId` must be defined for using this method!") 1573 raise Exception("Account ID required") 1574 1575 uLogger.debug("Requesting current actual pending orders. Wait, please...") 1576 1577 self.body = str({"accountId": self.accountId}) 1578 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/GetOrders" 1579 rawOrders = self.SendAPIRequest(ordersURL, reqType="POST")["orders"] 1580 1581 uLogger.debug("[{}] records about pending orders received".format(len(rawOrders))) 1582 1583 return rawOrders
Requesting current actual pending orders for current accountId.
REST API for pending (market) orders: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_GetOrders
Documentation: https://tinkoff.github.io/investAPI/orders/#getordersrequest
Returns
list of dictionaries with pending orders.
1585 def RequestStopOrders(self) -> list: 1586 """ 1587 Requesting current actual stop orders for current `accountId`. 1588 REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders 1589 Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest 1590 1591 :return: list of dictionaries with stop orders. 1592 """ 1593 if self.accountId is None or not self.accountId: 1594 uLogger.error("Variable `accountId` must be defined for using this method!") 1595 raise Exception("Account ID required") 1596 1597 uLogger.debug("Requesting current actual stop orders. Wait, please...") 1598 1599 self.body = str({"accountId": self.accountId}) 1600 ordersURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/GetStopOrders" 1601 rawStopOrders = self.SendAPIRequest(ordersURL, reqType="POST")["stopOrders"] 1602 1603 uLogger.debug("[{}] records about stop orders received".format(len(rawStopOrders))) 1604 1605 return rawStopOrders
Requesting current actual stop orders for current accountId.
REST API for opened stop-orders: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_GetStopOrders
Documentation: https://tinkoff.github.io/investAPI/stoporders/#getstopordersrequest
Returns
list of dictionaries with stop orders.
1607 def Overview(self, show: bool = False, details: str = "full") -> dict: 1608 """ 1609 Get portfolio: all open positions, orders and some statistics for current `accountId`. 1610 If `overviewFile`, `overviewDigestFile`, `overviewPositionsFile`, `overviewOrdersFile`, `overviewAnalyticsFile` 1611 are defined then also save information to file. 1612 1613 WARNING! It is not recommended to run this method too many times in a loop! The server receives 1614 many requests about the state of the portfolio, and then, based on the received data, a large number 1615 of calculation and statistics are collected. 1616 1617 :param show: if `False` then only dictionary returns, if `True` then show more debug information. 1618 :param details: how detailed should the information be? You should specify one of strings: 1619 `full` - shows full available information about portfolio status (by default), 1620 `positions` - shows only open positions, 1621 `digest` - show a short digest of the portfolio status, 1622 `analytics` - shows only the analytics section and the distribution of the portfolio by various categories, 1623 `orders` - shows only sections of open limits and stop orders. 1624 :return: dictionary with client's raw portfolio and some statistics. 1625 """ 1626 if self.accountId is None or not self.accountId: 1627 uLogger.error("Variable `accountId` must be defined for using this method!") 1628 raise Exception("Account ID required") 1629 1630 view = { 1631 "raw": { # --- raw portfolio responses from broker with user portfolio data: 1632 "headers": {}, # list of dictionaries, response headers without "positions" section 1633 "Currencies": [], # list of dictionaries, open trades with currencies from "positions" section 1634 "Shares": [], # list of dictionaries, open trades with shares from "positions" section 1635 "Bonds": [], # list of dictionaries, open trades with bonds from "positions" section 1636 "Etfs": [], # list of dictionaries, open trades with etfs from "positions" section 1637 "Futures": [], # list of dictionaries, open trades with futures from "positions" section 1638 "positions": {}, # raw response from broker: dictionary with current available or blocked currencies and instruments for client 1639 "orders": [], # raw response from broker: list of dictionaries with all pending (market) orders 1640 "stopOrders": [], # raw response from broker: list of dictionaries with all stop orders 1641 "currenciesCurrentPrices": {"rub": {"name": "Российский рубль", "currentPrice": 1.}}, # dict with prices of all currencies in RUB 1642 }, 1643 "stat": { # --- some statistics calculated using "raw" sections: 1644 "portfolioCostRUB": 0., # portfolio cost in RUB (Russian Rouble) 1645 "availableRUB": 0., # available rubles (without other currencies) 1646 "blockedRUB": 0., # blocked sum in Russian Rouble 1647 "totalChangesRUB": 0., # changes for all open trades in RUB 1648 "totalChangesPercentRUB": 0., # changes for all open trades in percents 1649 "allCurrenciesCostRUB": 0., # costs of all currencies (include rubles) in RUB 1650 "sharesCostRUB": 0., # costs of all shares in RUB 1651 "bondsCostRUB": 0., # costs of all bonds in RUB 1652 "etfsCostRUB": 0., # costs of all etfs in RUB 1653 "futuresCostRUB": 0., # costs of all futures in RUB 1654 "Currencies": [], # list of dictionaries of all currencies statistics 1655 "Shares": [], # list of dictionaries of all shares statistics 1656 "Bonds": [], # list of dictionaries of all bonds statistics 1657 "Etfs": [], # list of dictionaries of all etfs statistics 1658 "Futures": [], # list of dictionaries of all futures statistics 1659 "orders": [], # list of dictionaries of all pending (market) orders and it's parameters 1660 "stopOrders": [], # list of dictionaries of all stop orders and it's parameters 1661 "blockedCurrencies": {}, # dict with blocked instruments and currencies, e.g. {"rub": 1291.87, "usd": 6.21} 1662 "blockedInstruments": {}, # dict with blocked by FIGI, e.g. {} 1663 "funds": {}, # dict with free funds for trading (total - blocked), by all currencies, e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1664 }, 1665 "analytics": { # --- some analytics of portfolio: 1666 "distrByAssets": {}, # portfolio distribution by assets 1667 "distrByCompanies": {}, # portfolio distribution by companies 1668 "distrBySectors": {}, # portfolio distribution by sectors 1669 "distrByCurrencies": {}, # portfolio distribution by currencies 1670 "distrByCountries": {}, # portfolio distribution by countries 1671 } 1672 } 1673 1674 details = details.lower() 1675 availableDetails = ["full", "positions", "digest", "analytics", "orders"] 1676 if details not in availableDetails: 1677 details = "full" 1678 uLogger.debug("Requested incorrect details! The `details` must be one of this strings: {}. Details parameter set to `full` be default.".format(availableDetails)) 1679 1680 uLogger.debug("Requesting portfolio of a client. Wait, please...") 1681 1682 portfolioResponse = self.RequestPortfolio() # current user's portfolio (dict) 1683 view["raw"]["positions"] = self.RequestPositions() # current open positions by instruments (dict) 1684 view["raw"]["orders"] = self.RequestPendingOrders() # current actual pending orders (list) 1685 view["raw"]["stopOrders"] = self.RequestStopOrders() # current actual stop orders (list) 1686 1687 # save response headers without "positions" section: 1688 for key in portfolioResponse.keys(): 1689 if key != "positions": 1690 view["raw"]["headers"][key] = portfolioResponse[key] 1691 1692 else: 1693 continue 1694 1695 # Re-sorting and separating given raw instruments and currencies by type: https://tinkoff.github.io/investAPI/operations/#operation 1696 # Type of instrument must be only one of supported types in TKS_INSTRUMENTS 1697 for item in portfolioResponse["positions"]: 1698 if item["instrumentType"] == "currency": 1699 self.figi = item["figi"] 1700 curr = self.SearchByFIGI(requestPrice=False) 1701 1702 # current price of currency in RUB: 1703 view["raw"]["currenciesCurrentPrices"][curr["nominal"]["currency"]] = { 1704 "name": curr["name"], 1705 "currentPrice": NanoToFloat( 1706 item["currentPrice"]["units"], 1707 item["currentPrice"]["nano"] 1708 ), 1709 } 1710 1711 view["raw"]["Currencies"].append(item) 1712 1713 elif item["instrumentType"] == "share": 1714 view["raw"]["Shares"].append(item) 1715 1716 elif item["instrumentType"] == "bond": 1717 view["raw"]["Bonds"].append(item) 1718 1719 elif item["instrumentType"] == "etf": 1720 view["raw"]["Etfs"].append(item) 1721 1722 elif item["instrumentType"] == "futures": 1723 view["raw"]["Futures"].append(item) 1724 1725 else: 1726 continue 1727 1728 # how many volume of currencies (by ISO currency name) are blocked: 1729 for item in view["raw"]["positions"]["blocked"]: 1730 blocked = NanoToFloat(item["units"], item["nano"]) 1731 if blocked > 0: 1732 view["stat"]["blockedCurrencies"][item["currency"]] = blocked 1733 1734 # how many volume of instruments (by FIGI) are blocked: 1735 for item in view["raw"]["positions"]["securities"]: 1736 blocked = int(item["blocked"]) 1737 if blocked > 0: 1738 view["stat"]["blockedInstruments"][item["figi"]] = blocked 1739 1740 allBlocked = {**view["stat"]["blockedCurrencies"], **view["stat"]["blockedInstruments"]} 1741 1742 if "rub" in allBlocked.keys(): 1743 view["stat"]["blockedRUB"] = allBlocked["rub"] # blocked rubles 1744 1745 # --- saving current total amount in RUB of all currencies (with ruble), shares, bonds, etfs, futures and currencies: 1746 view["stat"]["allCurrenciesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountCurrencies"]["units"], portfolioResponse["totalAmountCurrencies"]["nano"]) 1747 view["stat"]["sharesCostRUB"] = NanoToFloat(portfolioResponse["totalAmountShares"]["units"], portfolioResponse["totalAmountShares"]["nano"]) 1748 view["stat"]["bondsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountBonds"]["units"], portfolioResponse["totalAmountBonds"]["nano"]) 1749 view["stat"]["etfsCostRUB"] = NanoToFloat(portfolioResponse["totalAmountEtf"]["units"], portfolioResponse["totalAmountEtf"]["nano"]) 1750 view["stat"]["futuresCostRUB"] = NanoToFloat(portfolioResponse["totalAmountFutures"]["units"], portfolioResponse["totalAmountFutures"]["nano"]) 1751 view["stat"]["portfolioCostRUB"] = sum([ 1752 view["stat"]["allCurrenciesCostRUB"], 1753 view["stat"]["sharesCostRUB"], 1754 view["stat"]["bondsCostRUB"], 1755 view["stat"]["etfsCostRUB"], 1756 view["stat"]["futuresCostRUB"], 1757 ]) 1758 1759 # --- calculating some portfolio statistics: 1760 byComp = {} # distribution by companies 1761 bySect = {} # distribution by sectors 1762 byCurr = {} # distribution by currencies (include RUB) 1763 unknownCountryName = "All other countries" # default name for instruments without "countryOfRisk" and "countryOfRiskName" 1764 byCountry = {unknownCountryName: {"cost": 0, "percent": 0.}} # distribution by countries (currencies are included in their countries) 1765 1766 for item in portfolioResponse["positions"]: 1767 self.figi = item["figi"] 1768 instrument = self.SearchByFIGI(requestPrice=False) # full raw info about instrument by FIGI 1769 1770 if instrument: 1771 if item["instrumentType"] == "currency" and instrument["nominal"]["currency"] in allBlocked.keys(): 1772 blocked = allBlocked[instrument["nominal"]["currency"]] # blocked volume of currency 1773 1774 elif item["instrumentType"] != "currency" and item["figi"] in allBlocked.keys(): 1775 blocked = allBlocked[item["figi"]] # blocked volume of other instruments 1776 1777 else: 1778 blocked = 0 1779 1780 volume = NanoToFloat(item["quantity"]["units"], item["quantity"]["nano"]) # available volume of instrument 1781 lots = NanoToFloat(item["quantityLots"]["units"], item["quantityLots"]["nano"]) # available volume in lots of instrument 1782 direction = "Long" if lots >= 0 else "Short" # direction of an instrument's position: short or long 1783 curPrice = NanoToFloat(item["currentPrice"]["units"], item["currentPrice"]["nano"]) # current instrument's price 1784 average = NanoToFloat(item["averagePositionPriceFifo"]["units"], item["averagePositionPriceFifo"]["nano"]) # current average position price 1785 profit = NanoToFloat(item["expectedYield"]["units"], item["expectedYield"]["nano"]) # expected profit at current moment 1786 currency = instrument["currency"] if (item["instrumentType"] == "share" or item["instrumentType"] == "etf" or item["instrumentType"] == "future") else instrument["nominal"]["currency"] # currency name rub, usd, eur etc. 1787 cost = (curPrice + NanoToFloat(item["currentNkd"]["units"], item["currentNkd"]["nano"])) * volume # current cost of all volume of instrument in basic asset 1788 baseCurrencyName = item["currentPrice"]["currency"] # name of base currency (rub) 1789 countryName = "[{}] {}".format(instrument["countryOfRisk"], instrument["countryOfRiskName"]) if "countryOfRisk" in instrument.keys() and "countryOfRiskName" in instrument.keys() and instrument["countryOfRisk"] and instrument["countryOfRiskName"] else unknownCountryName 1790 costRUB = cost if item["instrumentType"] == "currency" else cost * view["raw"]["currenciesCurrentPrices"][currency]["currentPrice"] # cost in rubles 1791 percentCostRUB = 100 * costRUB / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0. # instrument's part in percent of full portfolio cost 1792 1793 statData = { 1794 "figi": item["figi"], # FIGI from REST API "GetPortfolio" method 1795 "ticker": instrument["ticker"], # ticker by FIGI 1796 "currency": currency, # currency name rub, usd, eur etc. for instrument price 1797 "volume": volume, # available volume of instrument 1798 "lots": lots, # volume in lots of instrument 1799 "direction": direction, # direction of an instrument's position: short or long 1800 "blocked": blocked, # blocked volume of currency or instrument 1801 "currentPrice": curPrice, # current instrument's price in basic asset 1802 "average": average, # current average position price 1803 "cost": cost, # current cost of all volume of instrument in basic asset 1804 "baseCurrencyName": baseCurrencyName, # name of base currency (rub) 1805 "costRUB": costRUB, # cost of instrument in ruble 1806 "percentCostRUB": percentCostRUB, # instrument's part in percent of full portfolio cost in RUB 1807 "profit": profit, # expected profit at current moment 1808 "percentProfit": 100 * profit / (average * volume) if average != 0 and volume != 0 else 0, # expected percents of profit at current moment for this instrument 1809 "sector": instrument["sector"] if "sector" in instrument.keys() and instrument["sector"] else "other", 1810 "name": instrument["name"] if "name" in instrument.keys() else "", # human-readable names of instruments 1811 "isoCurrencyName": instrument["isoCurrencyName"] if "isoCurrencyName" in instrument.keys() else "", # ISO name for currencies only 1812 "country": countryName, # e.g. "[RU] Российская Федерация" or unknownCountryName 1813 "step": instrument["step"], # minimum price increment 1814 } 1815 1816 # adding distribution by unique countries: 1817 if statData["country"] not in byCountry.keys(): 1818 byCountry[statData["country"]] = {"cost": costRUB, "percent": percentCostRUB} 1819 1820 else: 1821 byCountry[statData["country"]]["cost"] += costRUB 1822 byCountry[statData["country"]]["percent"] += percentCostRUB 1823 1824 if item["instrumentType"] != "currency": 1825 # adding distribution by unique companies: 1826 if statData["name"]: 1827 if statData["name"] not in byComp.keys(): 1828 byComp[statData["name"]] = {"ticker": statData["ticker"], "cost": costRUB, "percent": percentCostRUB} 1829 1830 else: 1831 byComp[statData["name"]]["cost"] += costRUB 1832 byComp[statData["name"]]["percent"] += percentCostRUB 1833 1834 # adding distribution by unique sectors: 1835 if statData["sector"] not in bySect.keys(): 1836 bySect[statData["sector"]] = {"cost": costRUB, "percent": percentCostRUB} 1837 1838 else: 1839 bySect[statData["sector"]]["cost"] += costRUB 1840 bySect[statData["sector"]]["percent"] += percentCostRUB 1841 1842 # adding distribution by unique currencies: 1843 if currency not in byCurr.keys(): 1844 byCurr[currency] = { 1845 "name": view["raw"]["currenciesCurrentPrices"][currency]["name"], 1846 "cost": costRUB, 1847 "percent": percentCostRUB 1848 } 1849 1850 else: 1851 byCurr[currency]["cost"] += costRUB 1852 byCurr[currency]["percent"] += percentCostRUB 1853 1854 # saving statistics for every instrument: 1855 if item["instrumentType"] == "currency": 1856 view["stat"]["Currencies"].append(statData) 1857 1858 # update dict with free funds for trading (total - blocked) by currencies 1859 # e.g. {"rub": {"total": 10000.99, "totalCostRUB": 10000.99, "free": 1234.56, "freeCostRUB": 1234.56}, "usd": {"total": 250.55, "totalCostRUB": 15375.80, "free": 125.05, "freeCostRUB": 7687.50}} 1860 view["stat"]["funds"][currency] = { 1861 "total": volume, 1862 "totalCostRUB": costRUB, # total volume cost in rubles 1863 "free": volume - blocked, 1864 "freeCostRUB": costRUB * ((volume - blocked) / volume) if volume > 0 else 0, # free volume cost in rubles 1865 } 1866 1867 elif item["instrumentType"] == "share": 1868 view["stat"]["Shares"].append(statData) 1869 1870 elif item["instrumentType"] == "bond": 1871 view["stat"]["Bonds"].append(statData) 1872 1873 elif item["instrumentType"] == "etf": 1874 view["stat"]["Etfs"].append(statData) 1875 1876 elif item["instrumentType"] == "Futures": 1877 view["stat"]["Futures"].append(statData) 1878 1879 else: 1880 continue 1881 1882 # total changes in Russian Ruble: 1883 view["stat"]["availableRUB"] = view["stat"]["allCurrenciesCostRUB"] - sum([item["cost"] for item in view["stat"]["Currencies"]]) # available RUB without other currencies 1884 view["stat"]["totalChangesPercentRUB"] = NanoToFloat(view["raw"]["headers"]["expectedYield"]["units"], view["raw"]["headers"]["expectedYield"]["nano"]) if "expectedYield" in view["raw"]["headers"].keys() else 0. 1885 startCost = view["stat"]["portfolioCostRUB"] / (1 + view["stat"]["totalChangesPercentRUB"] / 100) 1886 view["stat"]["totalChangesRUB"] = view["stat"]["portfolioCostRUB"] - startCost 1887 view["stat"]["funds"]["rub"] = { 1888 "total": view["stat"]["availableRUB"], 1889 "totalCostRUB": view["stat"]["availableRUB"], 1890 "free": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1891 "freeCostRUB": view["stat"]["availableRUB"] - view["stat"]["blockedRUB"], 1892 } 1893 1894 # --- pending orders sector data: 1895 uniquePendingOrders = [] 1896 uniquePendingOrdersFIGIs = [] 1897 for item in view["raw"]["orders"]: 1898 if item["figi"] not in uniquePendingOrdersFIGIs: 1899 uniquePendingOrdersFIGIs.append(item["figi"]) 1900 uniquePendingOrders.append(item) 1901 1902 for item in uniquePendingOrders: 1903 self.figi = item["figi"] 1904 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1905 1906 if instrument: 1907 action = TKS_ORDER_DIRECTIONS[item["direction"]] 1908 orderType = TKS_ORDER_TYPES[item["orderType"]] 1909 orderState = TKS_ORDER_STATES[item["executionReportStatus"]] 1910 orderDate = item["orderDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1911 1912 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1913 if item["direction"] == "ORDER_DIRECTION_BUY": 1914 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1915 1916 else: 1917 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1918 1919 # requested price for order execution: 1920 target = NanoToFloat(item["initialSecurityPrice"]["units"], item["initialSecurityPrice"]["nano"]) 1921 1922 # necessary changes in percent to reach target from current price: 1923 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1924 1925 view["stat"]["orders"].append({ 1926 "orderID": item["orderId"], # orderId number parameter of current order 1927 "figi": item["figi"], # FIGI identification 1928 "ticker": instrument["ticker"], # ticker name by FIGI 1929 "lotsRequested": item["lotsRequested"], # requested lots value 1930 "lotsExecuted": item["lotsExecuted"], # how many lots are executed 1931 "currentPrice": lastPrice, # current instrument's price for defined action 1932 "targetPrice": target, # requested price for order execution in base currency 1933 "baseCurrencyName": item["initialSecurityPrice"]["currency"], # name of base currency 1934 "percentChanges": changes, # changes in percent to target from current price 1935 "currency": item["currency"], # instrument's currency name 1936 "action": action, # sell / buy / Unknown from TKS_ORDER_DIRECTIONS 1937 "type": orderType, # type of order from TKS_ORDER_TYPES 1938 "status": orderState, # order status from TKS_ORDER_STATES 1939 "date": orderDate, # string with order date and time from UTC format (without nano seconds part) 1940 }) 1941 1942 # --- stop orders sector data: 1943 uniqueStopOrders = [] 1944 uniqueStopOrdersFIGIs = [] 1945 for item in view["raw"]["stopOrders"]: 1946 if item["figi"] not in uniqueStopOrdersFIGIs: 1947 uniqueStopOrdersFIGIs.append(item["figi"]) 1948 uniqueStopOrders.append(item) 1949 1950 for item in uniqueStopOrders: 1951 self.figi = item["figi"] 1952 instrument = self.SearchByFIGI(requestPrice=True) # full raw info about instrument by FIGI 1953 1954 if instrument: 1955 action = TKS_STOP_ORDER_DIRECTIONS[item["direction"]] 1956 orderType = TKS_STOP_ORDER_TYPES[item["orderType"]] 1957 createDate = item["createDate"].replace("T", " ").replace("Z", "").split(".")[0] # date in UTC format, e.g. "2022-12-31T23:59:59.123456Z" 1958 1959 # hack: server response can't contain "expirationTime" key if it is not "Until date" type of stop order 1960 if "expirationTime" in item.keys(): 1961 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE"] 1962 expDate = item["expirationTime"].replace("T", " ").replace("Z", "").split(".")[0] 1963 1964 else: 1965 expType = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL"] 1966 expDate = TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"] 1967 1968 # current instrument's price (last sellers order if buy, and last buyers order if sell): 1969 if item["direction"] == "STOP_ORDER_DIRECTION_BUY": 1970 lastPrice = instrument["currentPrice"]["sell"][0]["price"] if instrument["currentPrice"]["sell"] else "N/A" 1971 1972 else: 1973 lastPrice = instrument["currentPrice"]["buy"][0]["price"] if instrument["currentPrice"]["buy"] else "N/A" 1974 1975 # requested price when stop-order executed: 1976 target = NanoToFloat(item["stopPrice"]["units"], item["stopPrice"]["nano"]) 1977 1978 # price for limit-order, set up when stop-order executed: 1979 limit = NanoToFloat(item["price"]["units"], item["price"]["nano"]) 1980 1981 # necessary changes in percent to reach target from current price: 1982 changes = 100 * (lastPrice - target) / target if lastPrice != "N/A" and target > 0 else 0 1983 1984 view["stat"]["stopOrders"].append({ 1985 "orderID": item["stopOrderId"], # stopOrderId number parameter of current stop-order 1986 "figi": item["figi"], # FIGI identification 1987 "ticker": instrument["ticker"], # ticker name by FIGI 1988 "lotsRequested": item["lotsRequested"], # requested lots value 1989 "currentPrice": lastPrice, # current instrument's price for defined action 1990 "targetPrice": target, # requested price for stop-order execution in base currency 1991 "limitPrice": limit, # price for limit-order, set up when stop-order executed, 0 if market order 1992 "baseCurrencyName": item["stopPrice"]["currency"], # name of base currency 1993 "percentChanges": changes, # changes in percent to target from current price 1994 "currency": item["currency"], # instrument's currency name 1995 "action": action, # sell / buy / Unknown from TKS_STOP_ORDER_DIRECTIONS 1996 "type": orderType, # type of order from TKS_STOP_ORDER_TYPES 1997 "expType": expType, # expiration type of stop-order from TKS_STOP_ORDER_EXPIRATION_TYPES 1998 "createDate": createDate, # string with created order date and time from UTC format (without nano seconds part) 1999 "expDate": expDate, # string with expiration order date and time from UTC format (without nano seconds part) 2000 }) 2001 2002 # --- calculating data for analytics section: 2003 # portfolio distribution by assets: 2004 view["analytics"]["distrByAssets"] = { 2005 "Ruble": { 2006 "uniques": 1, 2007 "cost": view["stat"]["availableRUB"], 2008 "percent": 100 * view["stat"]["availableRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2009 }, 2010 "Currencies": { 2011 "uniques": len(view["stat"]["Currencies"]), # all foreign currencies without RUB 2012 "cost": view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"], 2013 "percent": 100 * (view["stat"]["allCurrenciesCostRUB"] - view["stat"]["availableRUB"]) / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2014 }, 2015 "Shares": { 2016 "uniques": len(view["stat"]["Shares"]), 2017 "cost": view["stat"]["sharesCostRUB"], 2018 "percent": 100 * view["stat"]["sharesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2019 }, 2020 "Bonds": { 2021 "uniques": len(view["stat"]["Bonds"]), 2022 "cost": view["stat"]["bondsCostRUB"], 2023 "percent": 100 * view["stat"]["bondsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2024 }, 2025 "Etfs": { 2026 "uniques": len(view["stat"]["Etfs"]), 2027 "cost": view["stat"]["etfsCostRUB"], 2028 "percent": 100 * view["stat"]["etfsCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2029 }, 2030 "Futures": { 2031 "uniques": len(view["stat"]["Futures"]), 2032 "cost": view["stat"]["futuresCostRUB"], 2033 "percent": 100 * view["stat"]["futuresCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2034 }, 2035 } 2036 2037 # portfolio distribution by companies: 2038 view["analytics"]["distrByCompanies"]["All money cash"] = { 2039 "ticker": "", 2040 "cost": view["stat"]["allCurrenciesCostRUB"], 2041 "percent": 100 * view["stat"]["allCurrenciesCostRUB"] / view["stat"]["portfolioCostRUB"] if view["stat"]["portfolioCostRUB"] > 0 else 0., 2042 } 2043 view["analytics"]["distrByCompanies"].update(byComp) 2044 2045 # portfolio distribution by sectors: 2046 view["analytics"]["distrBySectors"]["All money cash"] = { 2047 "cost": view["analytics"]["distrByCompanies"]["All money cash"]["cost"], 2048 "percent": view["analytics"]["distrByCompanies"]["All money cash"]["percent"], 2049 } 2050 view["analytics"]["distrBySectors"].update(bySect) 2051 2052 # portfolio distribution by currencies: 2053 if "rub" not in view["analytics"]["distrByCurrencies"].keys(): 2054 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by currencies` section. Server not returned current available rubles!") 2055 view["analytics"]["distrByCurrencies"]["rub"] = {"name": "Российский рубль", "cost": 0, "percent": 0} 2056 2057 view["analytics"]["distrByCurrencies"].update(byCurr) 2058 view["analytics"]["distrByCurrencies"]["rub"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2059 view["analytics"]["distrByCurrencies"]["rub"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2060 2061 # portfolio distribution by countries: 2062 if "[RU] Российская Федерация" not in view["analytics"]["distrByCountries"].keys(): 2063 uLogger.debug("Fast hack to avoid issues #71 in `Portfolio distribution by countries` section. Server not returned current available rubles!") 2064 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"] = {"cost": 0, "percent": 0} 2065 2066 view["analytics"]["distrByCountries"].update(byCountry) 2067 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["cost"] += view["analytics"]["distrByAssets"]["Ruble"]["cost"] 2068 view["analytics"]["distrByCountries"]["[RU] Российская Федерация"]["percent"] += view["analytics"]["distrByAssets"]["Ruble"]["percent"] 2069 2070 # --- Prepare text statistics overview in human-readable: 2071 if show: 2072 # Whatever the value `details`, header not changes: 2073 info = [ 2074 "# Client's portfolio\n\n", 2075 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 2076 "* **Account ID:** [{}]\n".format(self.accountId), 2077 ] 2078 2079 if details in ["full", "positions", "digest"]: 2080 info.extend([ 2081 "* **Portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2082 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n\n".format( 2083 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2084 view["stat"]["totalChangesRUB"], 2085 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2086 view["stat"]["totalChangesPercentRUB"], 2087 ), 2088 ]) 2089 2090 if details in ["full", "positions"]: 2091 info.extend([ 2092 "## Open positions\n\n", 2093 "| Ticker [FIGI] | Volume (blocked) | Lots | Curr. price | Avg. price | Current volume cost | Profit (%) |\n", 2094 "|-----------------------------|---------------------------------|----------|--------------|--------------|---------------------|------------------------------|\n", 2095 "| Ruble | {:>31} | | | | | |\n".format( 2096 "{:.2f} ({:.2f}) rub".format( 2097 view["stat"]["availableRUB"], 2098 view["stat"]["blockedRUB"], 2099 ) 2100 ) 2101 ]) 2102 2103 def _SplitStr(CostRUB: float = 0, typeStr: str = "", noTradeStr: str = "") -> list: 2104 return [ 2105 "| | | | | | | |\n", 2106 "| {:<27} | | | | | {:>19} | |\n".format( 2107 noTradeStr if noTradeStr else typeStr, 2108 "" if noTradeStr else "{:.2f} RUB".format(CostRUB), 2109 ), 2110 ] 2111 2112 def _InfoStr(data: dict, showCurrencyName: bool = False) -> str: 2113 return "| {:<27} | {:>31} | {:<8} | {:>12} | {:>12} | {:>19} | {:<28} |\n".format( 2114 "{} [{}]".format(data["ticker"], data["figi"]), 2115 "{:.2f} ({:.2f}) {}".format( 2116 data["volume"], 2117 data["blocked"], 2118 data["currency"], 2119 ) if showCurrencyName else "{:.0f} ({:.0f})".format( 2120 data["volume"], 2121 data["blocked"], 2122 ), 2123 "{:.4f}".format(data["lots"]) if showCurrencyName else "{:.0f}".format(data["lots"]), 2124 "{:.2f} {}".format(data["currentPrice"], data["baseCurrencyName"]) if data["currentPrice"] > 0 else "n/a", 2125 "{:.2f} {}".format(data["average"], data["baseCurrencyName"]) if data["average"] > 0 else "n/a", 2126 "{:.2f} {}".format(data["cost"], data["baseCurrencyName"]), 2127 "{}{:.2f} {} ({}{:.2f}%)".format( 2128 "+" if data["profit"] > 0 else "", 2129 data["profit"], data["baseCurrencyName"], 2130 "+" if data["percentProfit"] > 0 else "", 2131 data["percentProfit"], 2132 ), 2133 ) 2134 2135 # --- Show currencies section: 2136 if view["stat"]["Currencies"]: 2137 info.extend(_SplitStr(CostRUB=view["analytics"]["distrByAssets"]["Currencies"]["cost"], typeStr="**Currencies:**")) 2138 for item in view["stat"]["Currencies"]: 2139 info.append(_InfoStr(item, showCurrencyName=True)) 2140 2141 else: 2142 info.extend(_SplitStr(noTradeStr="**Currencies:** no trades")) 2143 2144 # --- Show shares section: 2145 if view["stat"]["Shares"]: 2146 info.extend(_SplitStr(CostRUB=view["stat"]["sharesCostRUB"], typeStr="**Shares:**")) 2147 2148 for item in view["stat"]["Shares"]: 2149 info.append(_InfoStr(item)) 2150 2151 else: 2152 info.extend(_SplitStr(noTradeStr="**Shares:** no trades")) 2153 2154 # --- Show bonds section: 2155 if view["stat"]["Bonds"]: 2156 info.extend(_SplitStr(CostRUB=view["stat"]["bondsCostRUB"], typeStr="**Bonds:**")) 2157 2158 for item in view["stat"]["Bonds"]: 2159 info.append(_InfoStr(item)) 2160 2161 else: 2162 info.extend(_SplitStr(noTradeStr="**Bonds:** no trades")) 2163 2164 # --- Show etfs section: 2165 if view["stat"]["Etfs"]: 2166 info.extend(_SplitStr(CostRUB=view["stat"]["etfsCostRUB"], typeStr="**Etfs:**")) 2167 2168 for item in view["stat"]["Etfs"]: 2169 info.append(_InfoStr(item)) 2170 2171 else: 2172 info.extend(_SplitStr(noTradeStr="**Etfs:** no trades")) 2173 2174 # --- Show futures section: 2175 if view["stat"]["Futures"]: 2176 info.extend(_SplitStr(CostRUB=view["stat"]["futuresCostRUB"], typeStr="**Futures:**")) 2177 2178 for item in view["stat"]["Futures"]: 2179 info.append(_InfoStr(item)) 2180 2181 else: 2182 info.extend(_SplitStr(noTradeStr="**Futures:** no trades")) 2183 2184 if details in ["full", "orders"]: 2185 # --- Show pending orders section: 2186 if view["stat"]["orders"]: 2187 info.extend([ 2188 "\n## Opened pending limit-orders: {}\n".format(len(view["stat"]["orders"])), 2189 "\n| Ticker [FIGI] | Order ID | Lots (exec.) | Current price (% delta) | Target price | Action | Type | Create date (UTC) |\n", 2190 "|-----------------------------|----------------|--------------|-------------------------|---------------|-----------|-----------|-------------------------|\n", 2191 ]) 2192 2193 for item in view["stat"]["orders"]: 2194 info.append("| {:<27} | {:<14} | {:<12} | {:>23} | {:>13} | {:<9} | {:<9} | {:<23} |\n".format( 2195 "{} [{}]".format(item["ticker"], item["figi"]), 2196 item["orderID"], 2197 "{} ({})".format(item["lotsRequested"], item["lotsExecuted"]), 2198 "{} {} ({}{:.2f}%)".format( 2199 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2200 item["baseCurrencyName"], 2201 "+" if item["percentChanges"] > 0 else "", 2202 float(item["percentChanges"]), 2203 ), 2204 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2205 item["action"], 2206 item["type"], 2207 item["date"], 2208 )) 2209 2210 else: 2211 info.append("\n## Total pending limit-orders: 0\n") 2212 2213 # --- Show stop orders section: 2214 if view["stat"]["stopOrders"]: 2215 info.extend([ 2216 "\n## Opened stop-orders: {}\n".format(len(view["stat"]["stopOrders"])), 2217 "\n| Ticker [FIGI] | Stop order ID | Lots | Current price (% delta) | Target price | Limit price | Action | Type | Expire type | Create date (UTC) | Expiration (UTC) |\n", 2218 "|-----------------------------|--------------------------------------|--------|-------------------------|---------------|---------------|-----------|-------------|--------------|---------------------|---------------------|\n", 2219 ]) 2220 2221 for item in view["stat"]["stopOrders"]: 2222 info.append("| {:<27} | {:<14} | {:<6} | {:>23} | {:>13} | {:>13} | {:<9} | {:<11} | {:<12} | {:<19} | {:<19} |\n".format( 2223 "{} [{}]".format(item["ticker"], item["figi"]), 2224 item["orderID"], 2225 item["lotsRequested"], 2226 "{} {} ({}{:.2f}%)".format( 2227 "{}".format(item["currentPrice"]) if isinstance(item["currentPrice"], str) else "{:.2f}".format(float(item["currentPrice"])), 2228 item["baseCurrencyName"], 2229 "+" if item["percentChanges"] > 0 else "", 2230 float(item["percentChanges"]), 2231 ), 2232 "{:.2f} {}".format(float(item["targetPrice"]), item["baseCurrencyName"]), 2233 "{:.2f} {}".format(float(item["limitPrice"]), item["baseCurrencyName"]) if item["limitPrice"] and item["limitPrice"] != item["targetPrice"] else TKS_ORDER_TYPES["ORDER_TYPE_MARKET"], 2234 item["action"], 2235 item["type"], 2236 item["expType"], 2237 item["createDate"], 2238 item["expDate"], 2239 )) 2240 2241 else: 2242 info.append("\n## Total stop-orders: 0\n") 2243 2244 if details in ["full", "analytics"]: 2245 # -- Show analytics section: 2246 if view["stat"]["portfolioCostRUB"] > 0: 2247 info.extend([ 2248 "\n# Analytics\n" 2249 "\n* **Current total portfolio cost:** {:.2f} RUB\n".format(view["stat"]["portfolioCostRUB"]), 2250 "* **Changes:** {}{:.2f} RUB ({}{:.2f}%)\n".format( 2251 "+" if view["stat"]["totalChangesRUB"] > 0 else "", 2252 view["stat"]["totalChangesRUB"], 2253 "+" if view["stat"]["totalChangesPercentRUB"] > 0 else "", 2254 view["stat"]["totalChangesPercentRUB"], 2255 ), 2256 "\n## Portfolio distribution by assets\n" 2257 "\n| Type | Uniques | Percent | Current cost |\n", 2258 "|------------|---------|---------|--------------------|\n", 2259 ]) 2260 2261 for key in view["analytics"]["distrByAssets"].keys(): 2262 if view["analytics"]["distrByAssets"][key]["cost"] > 0: 2263 info.append("| {:<10} | {:<7} | {:<7} | {:<18} |\n".format( 2264 key, 2265 view["analytics"]["distrByAssets"][key]["uniques"], 2266 "{:.2f}%".format(view["analytics"]["distrByAssets"][key]["percent"]), 2267 "{:.2f} rub".format(view["analytics"]["distrByAssets"][key]["cost"]), 2268 )) 2269 2270 maxLenNames = 3 + max([len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) for company in view["analytics"]["distrByCompanies"].keys()]) 2271 info.extend([ 2272 "\n## Portfolio distribution by companies\n" 2273 "\n| Company{} | Percent | Current cost |\n".format(" " * (maxLenNames - 7)), 2274 "|--------{}-|---------|--------------------|\n".format("-" * (maxLenNames - 7)), 2275 ]) 2276 2277 for company in view["analytics"]["distrByCompanies"].keys(): 2278 if view["analytics"]["distrByCompanies"][company]["cost"] > 0: 2279 nameLen = len(company) + len(view["analytics"]["distrByCompanies"][company]["ticker"]) 2280 info.append("| {} | {:<7} | {:<18} |\n".format( 2281 "{}{}{}".format( 2282 "[{}] ".format(view["analytics"]["distrByCompanies"][company]["ticker"]) if view["analytics"]["distrByCompanies"][company]["ticker"] else "", 2283 company, 2284 "" if nameLen == maxLenNames else "{}".format(" " * (maxLenNames - nameLen - 3) if view["analytics"]["distrByCompanies"][company]["ticker"] else " " * (maxLenNames - nameLen)), 2285 ), 2286 "{:.2f}%".format(view["analytics"]["distrByCompanies"][company]["percent"]), 2287 "{:.2f} rub".format(view["analytics"]["distrByCompanies"][company]["cost"]), 2288 )) 2289 2290 maxLenSectors = max([len(sector) for sector in view["analytics"]["distrBySectors"].keys()]) 2291 info.extend([ 2292 "\n## Portfolio distribution by sectors\n" 2293 "\n| Sector{} | Percent | Current cost |\n".format(" " * (maxLenSectors - 6)), 2294 "|-------{}-|---------|--------------------|\n".format("-" * (maxLenSectors - 6)), 2295 ]) 2296 2297 for sector in view["analytics"]["distrBySectors"].keys(): 2298 if view["analytics"]["distrBySectors"][sector]["cost"] > 0: 2299 info.append("| {}{} | {:<7} | {:<18} |\n".format( 2300 sector, 2301 "" if len(sector) == maxLenSectors else " " * (maxLenSectors - len(sector)), 2302 "{:.2f}%".format(view["analytics"]["distrBySectors"][sector]["percent"]), 2303 "{:.2f} rub".format(view["analytics"]["distrBySectors"][sector]["cost"]), 2304 )) 2305 2306 maxLenMoney = 3 + max([len(currency) + len(view["analytics"]["distrByCurrencies"][currency]["name"]) for currency in view["analytics"]["distrByCurrencies"].keys()]) 2307 info.extend([ 2308 "\n## Portfolio distribution by currencies\n" 2309 "\n| Instruments currencies{} | Percent | Current cost |\n".format(" " * (maxLenMoney - 22)), 2310 "|-----------------------{}-|---------|--------------------|\n".format("-" * (maxLenMoney - 22)), 2311 ]) 2312 2313 for curr in view["analytics"]["distrByCurrencies"].keys(): 2314 if view["analytics"]["distrByCurrencies"][curr]["cost"] > 0: 2315 nameLen = 3 + len(curr) + len(view["analytics"]["distrByCurrencies"][curr]["name"]) 2316 info.append("| {} | {:<7} | {:<18} |\n".format( 2317 "[{}] {}{}".format( 2318 curr, 2319 view["analytics"]["distrByCurrencies"][curr]["name"], 2320 "" if nameLen == maxLenMoney else " " * (maxLenMoney - nameLen), 2321 ), 2322 "{:.2f}%".format(view["analytics"]["distrByCurrencies"][curr]["percent"]), 2323 "{:.2f} rub".format(view["analytics"]["distrByCurrencies"][curr]["cost"]), 2324 )) 2325 2326 maxLenCountry = max(17, max([len(country) for country in view["analytics"]["distrByCountries"].keys()])) 2327 info.extend([ 2328 "\n## Portfolio distribution by countries\n" 2329 "\n| Assets by country{} | Percent | Current cost |\n".format(" " * (maxLenCountry - 17)), 2330 "|------------------{}-|---------|--------------------|\n".format("-" * (maxLenCountry - 17)), 2331 ]) 2332 2333 for country in view["analytics"]["distrByCountries"].keys(): 2334 if view["analytics"]["distrByCountries"][country]["cost"] > 0: 2335 nameLen = len(country) 2336 info.append("| {} | {:<7} | {:<18} |\n".format( 2337 "{}{}".format( 2338 country, 2339 "" if nameLen == maxLenCountry else " " * (maxLenCountry - nameLen), 2340 ), 2341 "{:.2f}%".format(view["analytics"]["distrByCountries"][country]["percent"]), 2342 "{:.2f} rub".format(view["analytics"]["distrByCountries"][country]["cost"]), 2343 )) 2344 2345 infoText = "".join(info) 2346 2347 uLogger.info(infoText) 2348 2349 if details == "full" and self.overviewFile: 2350 filename = self.overviewFile 2351 2352 elif details == "digest" and self.overviewDigestFile: 2353 filename = self.overviewDigestFile 2354 2355 elif details == "positions" and self.overviewPositionsFile: 2356 filename = self.overviewPositionsFile 2357 2358 elif details == "orders" and self.overviewOrdersFile: 2359 filename = self.overviewOrdersFile 2360 2361 elif details == "analytics" and self.overviewAnalyticsFile: 2362 filename = self.overviewAnalyticsFile 2363 2364 else: 2365 filename = "" 2366 2367 if filename: 2368 with open(filename, "w", encoding="UTF-8") as fH: 2369 fH.write(infoText) 2370 2371 uLogger.info("Client's portfolio was saved to file: [{}]".format(os.path.abspath(filename))) 2372 2373 return view
Get portfolio: all open positions, orders and some statistics for current accountId.
If overviewFile, overviewDigestFile, overviewPositionsFile, overviewOrdersFile, overviewAnalyticsFile
are defined then also save information to file.
WARNING! It is not recommended to run this method too many times in a loop! The server receives many requests about the state of the portfolio, and then, based on the received data, a large number of calculation and statistics are collected.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen show more debug information. - details: how detailed should the information be? You should specify one of strings:
full- shows full available information about portfolio status (by default),positions- shows only open positions,digest- show a short digest of the portfolio status,analytics- shows only the analytics section and the distribution of the portfolio by various categories,orders- shows only sections of open limits and stop orders.
Returns
dictionary with client's raw portfolio and some statistics.
2375 def Deals(self, start: str = None, end: str = None, show: bool = False, showCancelled: bool = True) -> tuple: 2376 """ 2377 Returns history operations between two given dates for current `accountId`. 2378 If `reportFile` string is not empty then also save human-readable report. 2379 Shows some statistical data of closed positions. 2380 2381 :param start: see docstring in `GetDatesAsString()` method 2382 :param end: see docstring in `GetDatesAsString()` method 2383 :param show: if `True` then also prints all records to the console. 2384 :param showCancelled: if `False` then remove information about cancelled operations from the deals report. 2385 :return: original list of dictionaries with history of deals records from API ("operations" key): 2386 https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2387 and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc. 2388 """ 2389 if self.accountId is None or not self.accountId: 2390 uLogger.error("Variable `accountId` must be defined for using this method!") 2391 raise Exception("Account ID required") 2392 2393 startDate, endDate = GetDatesAsString(start, end) # Example: ("2000-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2394 2395 uLogger.debug("Requesting history of a client's operations. Wait, please...") 2396 2397 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations 2398 dealsURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetOperations" 2399 self.body = str({"accountId": self.accountId, "from": startDate, "to": endDate}) 2400 ops = self.SendAPIRequest(dealsURL, reqType="POST")["operations"] # list of dict: operations returns by broker 2401 customStat = {} # custom statistics in additional to responseJSON 2402 2403 # --- output report in human-readable format: 2404 if show or self.reportFile: 2405 splitLine1 = "| | | | | |\n" # Summary section 2406 splitLine2 = "| | | | | | | | |\n" # Operations section 2407 nextDay = "" 2408 2409 info = ["# Client's operations\n\n* **Period:** from [{}] to [{}]\n\n## Summary (operations executed only)\n\n".format(startDate.split("T")[0], endDate.split("T")[0])] 2410 2411 if len(ops) > 0: 2412 customStat = { 2413 "opsCount": 0, # total operations count 2414 "buyCount": 0, # buy operations 2415 "sellCount": 0, # sell operations 2416 "buyTotal": {"rub": 0.}, # Buy sums in different currencies 2417 "sellTotal": {"rub": 0.}, # Sell sums in different currencies 2418 "payIn": {"rub": 0.}, # Deposit brokerage account 2419 "payOut": {"rub": 0.}, # Withdrawals 2420 "divs": {"rub": 0.}, # Dividends income 2421 "coupons": {"rub": 0.}, # Coupon's income 2422 "brokerCom": {"rub": 0.}, # Service commissions 2423 "serviceCom": {"rub": 0.}, # Service commissions 2424 "marginCom": {"rub": 0.}, # Margin commissions 2425 "allTaxes": {"rub": 0.}, # Sum of withholding taxes and corrections 2426 } 2427 2428 # --- calculating statistics depends on operations type in TKS_OPERATION_TYPES: 2429 for item in ops: 2430 if item["state"] == "OPERATION_STATE_EXECUTED": 2431 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2432 2433 # count buy operations: 2434 if "_BUY" in item["operationType"]: 2435 customStat["buyCount"] += 1 2436 2437 if item["payment"]["currency"] in customStat["buyTotal"].keys(): 2438 customStat["buyTotal"][item["payment"]["currency"]] += payment 2439 2440 else: 2441 customStat["buyTotal"][item["payment"]["currency"]] = payment 2442 2443 # count sell operations: 2444 elif "_SELL" in item["operationType"]: 2445 customStat["sellCount"] += 1 2446 2447 if item["payment"]["currency"] in customStat["sellTotal"].keys(): 2448 customStat["sellTotal"][item["payment"]["currency"]] += payment 2449 2450 else: 2451 customStat["sellTotal"][item["payment"]["currency"]] = payment 2452 2453 # count incoming operations: 2454 elif item["operationType"] in ["OPERATION_TYPE_INPUT"]: 2455 if item["payment"]["currency"] in customStat["payIn"].keys(): 2456 customStat["payIn"][item["payment"]["currency"]] += payment 2457 2458 else: 2459 customStat["payIn"][item["payment"]["currency"]] = payment 2460 2461 # count withdrawals operations: 2462 elif item["operationType"] in ["OPERATION_TYPE_OUTPUT"]: 2463 if item["payment"]["currency"] in customStat["payOut"].keys(): 2464 customStat["payOut"][item["payment"]["currency"]] += payment 2465 2466 else: 2467 customStat["payOut"][item["payment"]["currency"]] = payment 2468 2469 # count dividends income: 2470 elif item["operationType"] in ["OPERATION_TYPE_DIVIDEND", "OPERATION_TYPE_DIVIDEND_TRANSFER", "OPERATION_TYPE_DIV_EXT"]: 2471 if item["payment"]["currency"] in customStat["divs"].keys(): 2472 customStat["divs"][item["payment"]["currency"]] += payment 2473 2474 else: 2475 customStat["divs"][item["payment"]["currency"]] = payment 2476 2477 # count coupon's income: 2478 elif item["operationType"] in ["OPERATION_TYPE_COUPON", "OPERATION_TYPE_BOND_REPAYMENT_FULL", "OPERATION_TYPE_BOND_REPAYMENT"]: 2479 if item["payment"]["currency"] in customStat["coupons"].keys(): 2480 customStat["coupons"][item["payment"]["currency"]] += payment 2481 2482 else: 2483 customStat["coupons"][item["payment"]["currency"]] = payment 2484 2485 # count broker commissions: 2486 elif item["operationType"] in ["OPERATION_TYPE_BROKER_FEE", "OPERATION_TYPE_SUCCESS_FEE", "OPERATION_TYPE_TRACK_MFEE", "OPERATION_TYPE_TRACK_PFEE"]: 2487 if item["payment"]["currency"] in customStat["brokerCom"].keys(): 2488 customStat["brokerCom"][item["payment"]["currency"]] += payment 2489 2490 else: 2491 customStat["brokerCom"][item["payment"]["currency"]] = payment 2492 2493 # count service commissions: 2494 elif item["operationType"] in ["OPERATION_TYPE_SERVICE_FEE"]: 2495 if item["payment"]["currency"] in customStat["serviceCom"].keys(): 2496 customStat["serviceCom"][item["payment"]["currency"]] += payment 2497 2498 else: 2499 customStat["serviceCom"][item["payment"]["currency"]] = payment 2500 2501 # count margin commissions: 2502 elif item["operationType"] in ["OPERATION_TYPE_MARGIN_FEE"]: 2503 if item["payment"]["currency"] in customStat["marginCom"].keys(): 2504 customStat["marginCom"][item["payment"]["currency"]] += payment 2505 2506 else: 2507 customStat["marginCom"][item["payment"]["currency"]] = payment 2508 2509 # count withholding taxes: 2510 elif "_TAX" in item["operationType"]: 2511 if item["payment"]["currency"] in customStat["allTaxes"].keys(): 2512 customStat["allTaxes"][item["payment"]["currency"]] += payment 2513 2514 else: 2515 customStat["allTaxes"][item["payment"]["currency"]] = payment 2516 2517 else: 2518 continue 2519 2520 customStat["opsCount"] += customStat["buyCount"] + customStat["sellCount"] 2521 2522 # --- view "Actions" lines: 2523 info.extend([ 2524 "| 1 | 2 | 3 | 4 | 5 |\n", 2525 "|----------------------------|-------------------------------|------------------------------|----------------------|------------------------|\n", 2526 "| **Actions:** | Trades: {:<21} | Trading volumes: | | |\n".format(customStat["opsCount"]), 2527 "| | Buy: {:<22} | {:<28} | | |\n".format( 2528 "{} ({:.1f}%)".format(customStat["buyCount"], 100 * customStat["buyCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2529 " rub, buy: {:<16}".format("{:.2f}".format(customStat["buyTotal"]["rub"])) if customStat["buyTotal"]["rub"] != 0 else " —", 2530 ), 2531 "| | Sell: {:<21} | {:<28} | | |\n".format( 2532 "{} ({:.1f}%)".format(customStat["sellCount"], 100 * customStat["sellCount"] / customStat["opsCount"]) if customStat["opsCount"] != 0 else 0, 2533 " rub, sell: {:<13}".format("+{:.2f}".format(customStat["sellTotal"]["rub"])) if customStat["sellTotal"]["rub"] != 0 else " —", 2534 ), 2535 ]) 2536 2537 opsKeys = sorted(list(set(list(customStat["buyTotal"].keys()) + list(customStat["sellTotal"].keys())))) 2538 for key in opsKeys: 2539 if key == "rub": 2540 continue 2541 2542 info.extend([ 2543 "| | | {:<28} | | |\n".format( 2544 " {}, buy: {:<16}".format(key, "{:.2f}".format(customStat["buyTotal"][key]) if key and key in customStat["buyTotal"].keys() and customStat["buyTotal"][key] != 0 else 0) 2545 ), 2546 "| | | {:<28} | | |\n".format( 2547 " {}, sell: {:<13}".format(key, "+{:.2f}".format(customStat["sellTotal"][key]) if key and key in customStat["sellTotal"].keys() and customStat["sellTotal"][key] != 0 else 0) 2548 ), 2549 ]) 2550 2551 info.append(splitLine1) 2552 2553 def _InfoStr(data1: dict, data2: dict, data3: dict, data4: dict, cur: str = "") -> str: 2554 return "| | {:<29} | {:<28} | {:<20} | {:<22} |\n".format( 2555 " {}: {}{:.2f}".format(cur, "+" if data1[cur] > 0 else "", data1[cur]) if cur and cur in data1.keys() and data1[cur] != 0 else " —", 2556 " {}: {}{:.2f}".format(cur, "+" if data2[cur] > 0 else "", data2[cur]) if cur and cur in data2.keys() and data2[cur] != 0 else " —", 2557 " {}: {}{:.2f}".format(cur, "+" if data3[cur] > 0 else "", data3[cur]) if cur and cur in data3.keys() and data3[cur] != 0 else " —", 2558 " {}: {}{:.2f}".format(cur, "+" if data4[cur] > 0 else "", data4[cur]) if cur and cur in data4.keys() and data4[cur] != 0 else " —", 2559 ) 2560 2561 # --- view "Payments" lines: 2562 info.append("| **Payments:** | Deposit on broker account: | Withdrawals: | Dividends income: | Coupons income: |\n") 2563 paymentsKeys = sorted(list(set(list(customStat["payIn"].keys()) + list(customStat["payOut"].keys()) + list(customStat["divs"].keys()) + list(customStat["coupons"].keys())))) 2564 2565 for key in paymentsKeys: 2566 info.append(_InfoStr(customStat["payIn"], customStat["payOut"], customStat["divs"], customStat["coupons"], key)) 2567 2568 info.append(splitLine1) 2569 2570 # --- view "Commissions and taxes" lines: 2571 info.append("| **Commissions and taxes:** | Broker commissions: | Service commissions: | Margin commissions: | All taxes/corrections: |\n") 2572 comKeys = sorted(list(set(list(customStat["brokerCom"].keys()) + list(customStat["serviceCom"].keys()) + list(customStat["marginCom"].keys()) + list(customStat["allTaxes"].keys())))) 2573 2574 for key in comKeys: 2575 info.append(_InfoStr(customStat["brokerCom"], customStat["serviceCom"], customStat["marginCom"], customStat["allTaxes"], key)) 2576 2577 info.append(splitLine1) 2578 2579 info.extend([ 2580 "\n## All operations{}\n\n".format("" if showCancelled else " (without cancelled status)"), 2581 "| Date and time | FIGI | Ticker | Asset | Value | Payment | Status | Operation type |\n", 2582 "|---------------------|--------------|--------------|------------|-----------|-----------------|------------|--------------------------------------------------------------------|\n", 2583 ]) 2584 2585 else: 2586 info.append("Broker returned no operations during this period\n") 2587 2588 # --- view "Operations" section: 2589 for item in ops: 2590 if not showCancelled and TKS_OPERATION_STATES[item["state"]] == TKS_OPERATION_STATES["OPERATION_STATE_CANCELED"]: 2591 continue 2592 2593 else: 2594 self.figi = item["figi"] if item["figi"] else "" 2595 payment = NanoToFloat(item["payment"]["units"], item["payment"]["nano"]) 2596 instrument = self.SearchByFIGI(requestPrice=False) if self.figi else {} 2597 2598 # group of deals during one day: 2599 if nextDay and item["date"].split("T")[0] != nextDay: 2600 info.append(splitLine2) 2601 nextDay = "" 2602 2603 else: 2604 nextDay = item["date"].split("T")[0] # saving current day for splitting 2605 2606 info.append("| {:<19} | {:<12} | {:<12} | {:<10} | {:<9} | {:>15} | {:<10} | {:<66} |\n".format( 2607 item["date"].replace("T", " ").replace("Z", "").split(".")[0], 2608 self.figi if self.figi else "—", 2609 instrument["ticker"] if instrument else "—", 2610 instrument["type"] if instrument else "—", 2611 item["quantity"] if int(item["quantity"]) > 0 else "—", 2612 "{}{:.2f} {}".format("+" if payment > 0 else "", payment, item["payment"]["currency"]) if payment != 0 else "—", 2613 TKS_OPERATION_STATES[item["state"]], 2614 TKS_OPERATION_TYPES[item["operationType"]], 2615 )) 2616 2617 infoText = "".join(info) 2618 2619 if show: 2620 uLogger.info(infoText) 2621 2622 if self.reportFile: 2623 with open(self.reportFile, "w", encoding="UTF-8") as fH: 2624 fH.write(infoText) 2625 2626 uLogger.info("History of a client's operations are saved to file: [{}]".format(os.path.abspath(self.reportFile))) 2627 2628 return ops, customStat
Returns history operations between two given dates for current accountId.
If reportFile string is not empty then also save human-readable report.
Shows some statistical data of closed positions.
Parameters
- start: see docstring in
GetDatesAsString()method - end: see docstring in
GetDatesAsString()method - show: if
Truethen also prints all records to the console. - showCancelled: if
Falsethen remove information about cancelled operations from the deals report.
Returns
original list of dictionaries with history of deals records from API ("operations" key): https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetOperations and dictionary with custom stats: operations in different currencies, withdrawals, incomes etc.
2630 def History(self, start: str = None, end: str = None, interval: str = "hour", onlyMissing: bool = False, csvSep: str = ",", show: bool = False) -> pd.DataFrame: 2631 """ 2632 This method returns last history candles of the current instrument defined by `ticker` or `figi` (FIGI id). 2633 2634 History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. 2635 Warning! Broker server used ISO UTC time by default. 2636 2637 If `historyFile` is not `None` then method save history to file, otherwise return only Pandas DataFrame. 2638 Also, `historyFile` used to update history with `onlyMissing` parameter. 2639 2640 See also: `LoadHistory()` and `ShowHistoryChart()` methods. 2641 2642 :param start: see docstring in `GetDatesAsString()` method. 2643 :param end: see docstring in `GetDatesAsString()` method. 2644 :param interval: this is a candle interval. Current available values are `"1min"`, `"5min"`, `"15min"`, 2645 `"hour"`, `"day"`. Default: `"hour"`. 2646 :param onlyMissing: if `True` then add only last missing candles, do not request all history length from `start`. 2647 False by default. Warning! History appends only from last candle to current time 2648 with always update last candle! 2649 :param csvSep: separator if csv-file is used, `,` by default. 2650 :param show: if `True` then also prints Pandas DataFrame to the console. 2651 :return: Pandas DataFrame with prices history. Headers of columns are defined by default: 2652 `["date", "time", "open", "high", "low", "close", "volume"]`. 2653 """ 2654 strStartDate, strEndDate = GetDatesAsString(start, end) # example: ("2020-01-01T00:00:00Z", "2022-12-31T23:59:59Z") 2655 headers = ["date", "time", "open", "high", "low", "close", "volume"] # sequence and names of column headers 2656 history = None # empty pandas object for history 2657 2658 if interval not in TKS_CANDLE_INTERVALS.keys(): 2659 uLogger.error("Interval parameter must be string with current available values: `1min`, `5min`, `15min`, `hour` and `day`.") 2660 raise Exception("Incorrect value") 2661 2662 if not (self.ticker or self.figi): 2663 uLogger.error("Ticker or FIGI must be defined!") 2664 raise Exception("Ticker or FIGI required") 2665 2666 if self.ticker and not self.figi: 2667 instrumentByTicker = self.SearchByTicker(requestPrice=False, debug=False) 2668 self.figi = instrumentByTicker["figi"] if instrumentByTicker else "" 2669 2670 if self.figi and not self.ticker: 2671 instrumentByFIGI = self.SearchByFIGI(requestPrice=False, debug=False) 2672 self.ticker = instrumentByFIGI["ticker"] if instrumentByFIGI else "" 2673 2674 dtStart = datetime.strptime(strStartDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from start time string 2675 dtEnd = datetime.strptime(strEndDate, TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()) # datetime object from end time string 2676 if interval.lower() != "day": 2677 dtEnd += timedelta(seconds=1) # adds 1 sec for requests, because day end returned by `GetDatesAsString()` as 23:59:59 2678 2679 delta = dtEnd - dtStart # current UTC time minus last time in file 2680 deltaMinutes = delta.days * 1440 + delta.seconds // 60 # minutes between start and end dates 2681 2682 # calculate history length in candles: 2683 length = deltaMinutes // TKS_CANDLE_INTERVALS[interval][1] 2684 if deltaMinutes % TKS_CANDLE_INTERVALS[interval][1] > 0: 2685 length += 1 # to avoid fraction time 2686 2687 # calculate data blocks count: 2688 blocks = 1 if length < TKS_CANDLE_INTERVALS[interval][2] else 1 + length // TKS_CANDLE_INTERVALS[interval][2] 2689 2690 uLogger.debug("Original requested time period in local time: from [{}] to [{}]".format(start, end)) 2691 uLogger.debug("Requested time period is about from [{}] UTC to [{}] UTC".format(strStartDate, strEndDate)) 2692 uLogger.debug("Calculated history length: [{}], interval: [{}]".format(length, interval)) 2693 uLogger.debug("Data blocks, count: [{}], max candles in block: [{}]".format(blocks, TKS_CANDLE_INTERVALS[interval][2])) 2694 uLogger.debug("Requesting history candlesticks, ticker: [{}], FIGI: [{}]. Wait, please...".format(self.ticker, self.figi)) 2695 2696 tempOld = None # pandas object for old history, if --only-missing key present 2697 lastTime = None # datetime object of last old candle in file 2698 2699 if onlyMissing and self.historyFile is not None and self.historyFile and os.path.exists(self.historyFile): 2700 uLogger.debug("--only-missing key present, add only last missing candles...") 2701 uLogger.debug("History file will be updated: [{}]".format(os.path.abspath(self.historyFile))) 2702 2703 tempOld = pd.read_csv(self.historyFile, sep=csvSep, header=None, names=headers) 2704 2705 tempOld["date"] = pd.to_datetime(tempOld["date"]) # load date "as is" 2706 tempOld["date"] = tempOld["date"].dt.strftime("%Y.%m.%d") # convert date to string 2707 tempOld["time"] = pd.to_datetime(tempOld["time"]) # load time "as is" 2708 tempOld["time"] = tempOld["time"].dt.strftime("%H:%M") # convert time to string 2709 2710 # get last datetime object from last string in file or minus 1 delta if file is empty: 2711 if len(tempOld) > 0: 2712 lastTime = datetime.strptime(tempOld.date.iloc[-1] + " " + tempOld.time.iloc[-1], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2713 2714 else: 2715 lastTime = dtEnd - timedelta(days=1) # history file is empty, so last date set at -1 day 2716 2717 tempOld = tempOld[:-1] # always remove last old candle because it may be incompletely at the current time 2718 2719 responseJSONs = [] # raw history blocks of data 2720 2721 blockEnd = dtEnd 2722 for item in range(blocks): 2723 tail = length % TKS_CANDLE_INTERVALS[interval][2] if item + 1 == blocks else TKS_CANDLE_INTERVALS[interval][2] 2724 blockStart = blockEnd - timedelta(minutes=TKS_CANDLE_INTERVALS[interval][1] * tail) 2725 2726 uLogger.debug("[Block #{}/{}] time period: [{}] UTC - [{}] UTC".format( 2727 item + 1, blocks, blockStart.strftime(TKS_DATE_TIME_FORMAT), blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2728 )) 2729 2730 if blockStart == blockEnd: 2731 uLogger.debug("Skipped this zero-length block...") 2732 2733 else: 2734 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/MarketDataService/MarketDataService_GetCandles 2735 historyURL = self.server + r"/tinkoff.public.invest.api.contract.v1.MarketDataService/GetCandles" 2736 self.body = str({ 2737 "figi": self.figi, 2738 "from": blockStart.strftime(TKS_DATE_TIME_FORMAT), 2739 "to": blockEnd.strftime(TKS_DATE_TIME_FORMAT), 2740 "interval": TKS_CANDLE_INTERVALS[interval][0] 2741 }) 2742 responseJSON = self.SendAPIRequest(historyURL, reqType="POST", retry=1, pause=1, debug=False) 2743 2744 if "code" in responseJSON.keys(): 2745 uLogger.debug("An issue occurred and block #{}/{} is empty".format(item + 1, blocks)) 2746 2747 else: 2748 if start is not None and (start.lower() == "yesterday" or start == end) and interval == "day" and len(responseJSON["candles"]) > 1: 2749 responseJSON["candles"] = responseJSON["candles"][:-1] # removes last candle for "yesterday" request 2750 2751 responseJSONs = responseJSON["candles"] + responseJSONs # add more old history behind newest dates 2752 2753 blockEnd = blockStart 2754 2755 printCount = len(responseJSONs) # candles to show in console 2756 if responseJSONs: 2757 tempHistory = pd.DataFrame( 2758 data={ 2759 "date": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2760 "time": [pd.to_datetime(item["time"]).astimezone(tzutc()) for item in responseJSONs], 2761 "open": [NanoToFloat(item["open"]["units"], item["open"]["nano"]) for item in responseJSONs], 2762 "high": [NanoToFloat(item["high"]["units"], item["high"]["nano"]) for item in responseJSONs], 2763 "low": [NanoToFloat(item["low"]["units"], item["low"]["nano"]) for item in responseJSONs], 2764 "close": [NanoToFloat(item["close"]["units"], item["close"]["nano"]) for item in responseJSONs], 2765 "volume": [int(item["volume"]) for item in responseJSONs], 2766 }, 2767 index=range(len(responseJSONs)), 2768 columns=["date", "time", "open", "high", "low", "close", "volume"], 2769 ) 2770 tempHistory["date"] = tempHistory["date"].dt.strftime("%Y.%m.%d") 2771 tempHistory["time"] = tempHistory["time"].dt.strftime("%H:%M") 2772 2773 # append only newest candles to old history if --only-missing key present: 2774 if onlyMissing and tempOld is not None and lastTime is not None: 2775 index = 0 # find start index in tempHistory data: 2776 2777 for i, item in tempHistory.iterrows(): 2778 curTime = datetime.strptime(item["date"] + " " + item["time"], "%Y.%m.%d %H:%M").replace(tzinfo=tzutc()) 2779 2780 if curTime == lastTime: 2781 uLogger.debug("History will be updated starting from the date: [{}]".format(curTime.strftime(TKS_PRINT_DATE_TIME_FORMAT))) 2782 index = i 2783 printCount = index + 1 2784 break 2785 2786 history = pd.concat([tempOld, tempHistory[index:]], ignore_index=True) 2787 2788 else: 2789 history = tempHistory # if no `--only-missing` key then load full data from server 2790 2791 uLogger.debug("Last 3 rows of received history:\n{}".format(pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-3:], max_cols=20, index=False))) 2792 2793 if history is not None and not history.empty: 2794 if show: 2795 uLogger.info("Here's requested history between [{}] UTC and [{}] UTC, not-empty candles count: [{}]\n{}".format( 2796 strStartDate.replace("T", " ").replace("Z", ""), strEndDate.replace("T", " ").replace("Z", ""), len(history[-printCount:]), 2797 pd.DataFrame.to_string(history[["date", "time", "open", "high", "low", "close", "volume"]][-printCount:], max_cols=20, index=False), 2798 )) 2799 2800 else: 2801 uLogger.warning("Received an empty candles history!") 2802 2803 if self.historyFile is not None: 2804 if history is not None and not history.empty: 2805 history.to_csv(self.historyFile, sep=csvSep, index=False, header=None) 2806 uLogger.info("Ticker [{}], FIGI [{}], tf: [{}], history saved: [{}]".format(self.ticker, self.figi, interval, os.path.abspath(self.historyFile))) 2807 2808 else: 2809 uLogger.warning("Empty history received! File NOT updated: [{}]".format(os.path.abspath(self.historyFile))) 2810 2811 else: 2812 uLogger.debug("--output key is not defined. Parsed history file not saved to file, only Pandas DataFrame returns.") 2813 2814 return history
This method returns last history candles of the current instrument defined by ticker or figi (FIGI id).
History returned between two given dates: start and end. Minimum requested date in the past is 1970-01-01.
Warning! Broker server used ISO UTC time by default.
If historyFile is not None then method save history to file, otherwise return only Pandas DataFrame.
Also, historyFile used to update history with onlyMissing parameter.
See also: LoadHistory() and ShowHistoryChart() methods.
Parameters
- start: see docstring in
GetDatesAsString()method. - end: see docstring in
GetDatesAsString()method. - interval: this is a candle interval. Current available values are
"1min","5min","15min","hour","day". Default:"hour". - onlyMissing: if
Truethen add only last missing candles, do not request all history length fromstart. False by default. Warning! History appends only from last candle to current time with always update last candle! - csvSep: separator if csv-file is used,
,by default. - show: if
Truethen also prints Pandas DataFrame to the console.
Returns
Pandas DataFrame with prices history. Headers of columns are defined by default:
["date", "time", "open", "high", "low", "close", "volume"].
2816 def LoadHistory(self, filePath: str) -> pd.DataFrame: 2817 """ 2818 Load candles history from csv-file and return Pandas DataFrame object. 2819 2820 See also: `History()` and `ShowHistoryChart()` methods. 2821 2822 :param filePath: path to csv-file to open. 2823 """ 2824 loadedHistory = None # init candles data object 2825 2826 uLogger.debug("Loading candles history with PriceGenerator module. Wait, please...") 2827 2828 if os.path.exists(filePath): 2829 loadedHistory = self.priceModel.LoadFromFile(filePath) # load data and get chain of candles as Pandas DataFrame 2830 2831 tfStr = self.priceModel.FormattedDelta( 2832 self.priceModel.timeframe, 2833 "{days} days {hours}h {minutes}m {seconds}s", 2834 ) if self.priceModel.timeframe >= timedelta(days=1) else self.priceModel.FormattedDelta( 2835 self.priceModel.timeframe, 2836 "{hours}h {minutes}m {seconds}s", 2837 ) 2838 2839 if loadedHistory is not None and not loadedHistory.empty: 2840 uLogger.info("Rows count loaded: [{}], detected timeframe of candles: [{}]. Showing some last rows:\n{}".format( 2841 len(loadedHistory), 2842 tfStr, 2843 pd.DataFrame.to_string(loadedHistory[-10:], max_cols=20)), 2844 ) 2845 2846 else: 2847 uLogger.warning("It was loaded an empty history! Path: [{}]".format(os.path.abspath(filePath))) 2848 2849 else: 2850 uLogger.error("File with candles history does not exist! Check the path: [{}]".format(filePath)) 2851 2852 return loadedHistory
Load candles history from csv-file and return Pandas DataFrame object.
See also: History() and ShowHistoryChart() methods.
Parameters
- filePath: path to csv-file to open.
2854 def ShowHistoryChart(self, candles: Union[str, pd.DataFrame] = None, interact: bool = True, openInBrowser: bool = False) -> None: 2855 """ 2856 Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file. 2857 2858 Self variable `htmlHistoryFile` can be use as html-file name to save interaction or non-interaction chart. 2859 Default: `index.html` (both for interact and non-interact candlesticks chart). 2860 2861 See also: `History()` and `LoadHistory()` methods. 2862 2863 :param candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object. 2864 :param interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. 2865 See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters 2866 If False then chain of candlesticks will render as not interactive Google Candlestick chart. 2867 See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template 2868 :param openInBrowser: if True then immediately open chart in default browser, otherwise only path to 2869 html-file prints to console. False by default, to avoid issues with `permissions denied` to html-file. 2870 """ 2871 if isinstance(candles, str): 2872 self.priceModel.prices = self.LoadHistory(filePath=candles) # load candles chain from file 2873 self.priceModel.ticker = os.path.basename(candles) # use filename as ticker name in PriceGenerator 2874 2875 elif isinstance(candles, pd.DataFrame): 2876 self.priceModel.prices = candles # set candles chain from variable 2877 self.priceModel.ticker = self.ticker # use current TKSBrokerAPI ticker as ticker name in PriceGenerator 2878 2879 if "datetime" not in candles.columns: 2880 self.priceModel.prices["datetime"] = pd.to_datetime(candles.date + ' ' + candles.time, utc=True) # PriceGenerator uses "datetime" column with date and time 2881 2882 else: 2883 uLogger.error("`candles` variable must be path string to the csv-file with candles in OHLCV-model or like Pandas Dataframe object!") 2884 raise Exception("Incorrect value") 2885 2886 self.priceModel.horizon = len(self.priceModel.prices) # use length of candles data as horizon in PriceGenerator 2887 2888 if interact: 2889 uLogger.debug("Rendering interactive candles chart. Wait, please...") 2890 2891 self.priceModel.RenderBokeh(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2892 2893 else: 2894 uLogger.debug("Rendering non-interactive candles chart. Wait, please...") 2895 2896 self.priceModel.RenderGoogle(fileName=self.htmlHistoryFile, viewInBrowser=openInBrowser) 2897 2898 uLogger.info("Rendered candles chart: [{}]".format(os.path.abspath(self.htmlHistoryFile)))
Render an HTML-file with interact or non-interact candlesticks chart. Candles may be path to the csv-file.
Self variable htmlHistoryFile can be use as html-file name to save interaction or non-interaction chart.
Default: index.html (both for interact and non-interact candlesticks chart).
See also: History() and LoadHistory() methods.
Parameters
- candles: string to csv-file with candles in OHLCV-model or like Pandas Dataframe object.
- interact: if True (default) then chain of candlesticks will render as interactive Bokeh chart. See examples: https://github.com/Tim55667757/PriceGenerator#overriding-parameters If False then chain of candlesticks will render as not interactive Google Candlestick chart. See examples: https://github.com/Tim55667757/PriceGenerator#statistics-and-chart-on-a-simple-template
- openInBrowser: if True then immediately open chart in default browser, otherwise only path to
html-file prints to console. False by default, to avoid issues with
permissions deniedto html-file.
2900 def Trade(self, operation: str, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2901 """ 2902 Universal method to create market order and make deal at the current price for current `accountId`. Returns JSON data with response. 2903 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2904 2905 See also: `Order()` docstring. More simple methods than `Trade()` are `Buy()` and `Sell()`. 2906 2907 :param operation: string "Buy" or "Sell". 2908 :param lots: volume, integer count of lots >= 1. 2909 :param tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter `targetPrice` in `self.Order()`. 2910 :param sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter `targetPrice` in `self.Order()`. 2911 :param expDate: string "Undefined" by default or local date in future, 2912 it is a string with format `%Y-%m-%d %H:%M:%S`. 2913 :return: JSON with response from broker server. 2914 """ 2915 if self.accountId is None or not self.accountId: 2916 uLogger.error("Variable `accountId` must be defined for using this method!") 2917 raise Exception("Account ID required") 2918 2919 if operation is None or not operation or operation not in ("Buy", "Sell"): 2920 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 2921 raise Exception("Incorrect value") 2922 2923 if lots is None or lots < 1: 2924 uLogger.warning("You must define trade volume > 0: integer count of lots! For current operation lots reset to 1.") 2925 lots = 1 2926 2927 if tp is None or tp < 0: 2928 tp = 0 2929 2930 if sl is None or sl < 0: 2931 sl = 0 2932 2933 if expDate is None or not expDate: 2934 expDate = "Undefined" 2935 2936 if not (self.ticker or self.figi): 2937 uLogger.error("Ticker or FIGI must be defined!") 2938 raise Exception("Ticker or FIGI required") 2939 2940 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 2941 self.ticker = instrument["ticker"] 2942 self.figi = instrument["figi"] 2943 2944 uLogger.debug("Opening [{}] market order: ticker [{}], FIGI [{}], lots [{}], TP [{:.4f}], SL [{:.4f}], expiration date of TP/SL orders [{}]. Wait, please...".format(operation, self.ticker, self.figi, lots, tp, sl, expDate)) 2945 2946 openTradeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 2947 self.body = str({ 2948 "figi": self.figi, 2949 "quantity": str(lots), 2950 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 2951 "accountId": str(self.accountId), 2952 "orderType": "ORDER_TYPE_MARKET", # see: TKS_ORDER_TYPES 2953 }) 2954 response = self.SendAPIRequest(openTradeURL, reqType="POST", retry=0, debug=False) 2955 2956 if "orderId" in response.keys(): 2957 uLogger.info("[{}] market order [{}] was executed: ticker [{}], FIGI [{}], lots [{}]. Total order price: [{:.4f} {}] (with commission: [{:.2f} {}]). Average price of lot: [{:.2f} {}]".format( 2958 operation, response["orderId"], 2959 self.ticker, self.figi, lots, 2960 NanoToFloat(response["totalOrderAmount"]["units"], response["totalOrderAmount"]["nano"]), response["totalOrderAmount"]["currency"], 2961 NanoToFloat(response["initialCommission"]["units"], response["initialCommission"]["nano"]), response["initialCommission"]["currency"], 2962 NanoToFloat(response["executedOrderPrice"]["units"], response["executedOrderPrice"]["nano"]), response["executedOrderPrice"]["currency"], 2963 )) 2964 2965 else: 2966 uLogger.warning("Not `oK` status received! Market order not created. See full debug log or try again and open order later.") 2967 2968 if tp > 0: 2969 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=tp, limitPrice=tp, stopType="TP", expDate=expDate) 2970 2971 if sl > 0: 2972 self.Order(operation="Sell" if operation == "Buy" else "Buy", orderType="Stop", lots=lots, targetPrice=sl, limitPrice=sl, stopType="SL", expDate=expDate) 2973 2974 return response
Universal method to create market order and make deal at the current price for current accountId. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() docstring. More simple methods than Trade() are Buy() and Sell().
Parameters
- operation: string "Buy" or "Sell".
- lots: volume, integer count of lots >= 1.
- tp: float > 0, target price for stop-order with "TP" type. It used as take profit parameter
targetPriceinself.Order(). - sl: float > 0, target price for stop-order with "SL" type. It used as stop loss parameter
targetPriceinself.Order(). - expDate: string "Undefined" by default or local date in future,
it is a string with format
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2976 def Buy(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2977 """ 2978 More simple method than `Trade()`. Create `Buy` market order and make deal at the current price. Returns JSON data with response. 2979 If `tp` or `sl` > 0, then in additional will opens stop-orders with "TP" and "SL" flags for `stopType` parameter. 2980 2981 See also: `Order()` and `Trade()` docstrings. 2982 2983 :param lots: volume, integer count of lots >= 1. 2984 :param tp: float > 0, take profit price of stop-order. 2985 :param sl: float > 0, stop loss price of stop-order. 2986 :param expDate: it's a local date in future. 2987 String has a format like this: `%Y-%m-%d %H:%M:%S`. 2988 :return: JSON with response from broker server. 2989 """ 2990 return self.Trade(operation="Buy", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Buy market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will opens stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
2992 def Sell(self, lots: int = 1, tp: float = 0., sl: float = 0., expDate: str = "Undefined") -> dict: 2993 """ 2994 More simple method than `Trade()`. Create `Sell` market order and make deal at the current price. Returns JSON data with response. 2995 If `tp` or `sl` > 0, then in additional will open stop-orders with "TP" and "SL" flags for `stopType` parameter. 2996 2997 See also: `Order()` and `Trade()` docstrings. 2998 2999 :param lots: volume, integer count of lots >= 1. 3000 :param tp: float > 0, take profit price of stop-order. 3001 :param sl: float > 0, stop loss price of stop-order. 3002 :param expDate: it's a local date in the future. 3003 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3004 :return: JSON with response from broker server. 3005 """ 3006 return self.Trade(operation="Sell", lots=lots, tp=tp, sl=sl, expDate=expDate)
More simple method than Trade(). Create Sell market order and make deal at the current price. Returns JSON data with response.
If tp or sl > 0, then in additional will open stop-orders with "TP" and "SL" flags for stopType parameter.
See also: Order() and Trade() docstrings.
Parameters
- lots: volume, integer count of lots >= 1.
- tp: float > 0, take profit price of stop-order.
- sl: float > 0, stop loss price of stop-order.
- expDate: it's a local date in the future.
String has a format like this:
%Y-%m-%d %H:%M:%S.
Returns
JSON with response from broker server.
3008 def CloseTrades(self, tickers: list, portfolio: dict = None) -> None: 3009 """ 3010 Close position of given instruments. 3011 3012 :param tickers: tickers list of instruments that must be closed. 3013 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3014 This avoids unnecessary downloading data from the server. 3015 """ 3016 if not tickers: 3017 uLogger.info("Tickers list is empty, nothing to close.") 3018 3019 else: 3020 if portfolio is None or not portfolio: 3021 portfolio = self.Overview(show=False) 3022 3023 allOpenedTickers = [item["ticker"] for iType in TKS_INSTRUMENTS for item in portfolio["stat"][iType]] 3024 uLogger.debug("All opened instruments by it's tickers names: {}".format(allOpenedTickers)) 3025 3026 for ticker in tickers: 3027 if ticker not in allOpenedTickers: 3028 uLogger.warning("Instrument with ticker [{}] not in open positions list!".format(ticker)) 3029 continue 3030 3031 # search open trade info about instrument by ticker: 3032 instrument = {} 3033 for iType in TKS_INSTRUMENTS: 3034 if instrument: 3035 break 3036 3037 for item in portfolio["stat"][iType]: 3038 if item["ticker"] == ticker: 3039 instrument = item 3040 break 3041 3042 if instrument: 3043 self.ticker = ticker 3044 self.figi = instrument["figi"] 3045 3046 uLogger.debug("Closing trade of instrument: ticker [{}], FIGI[{}], lots [{}]{}. Wait, please...".format( 3047 self.ticker, 3048 self.figi, 3049 int(instrument["volume"]), 3050 ", blocked [{}]".format(instrument["blocked"]) if instrument["blocked"] > 0 else "", 3051 )) 3052 3053 tradeLots = abs(instrument["lots"]) - instrument["blocked"] # available volumes in lots for close operation 3054 3055 if tradeLots > 0: 3056 if instrument["blocked"] > 0: 3057 uLogger.warning("Just for your information: there are [{}] lots blocked for instrument [{}]! Available only [{}] lots to closing trade.".format( 3058 instrument["blocked"], 3059 self.ticker, 3060 tradeLots, 3061 )) 3062 3063 # if direction is "Long" then we need sell, if direction is "Short" then we need buy: 3064 self.Trade(operation="Sell" if instrument["direction"] == "Long" else "Buy", lots=tradeLots) 3065 3066 else: 3067 uLogger.warning("There are no available lots for instrument [{}] to closing trade at this moment! Try again later or cancel some orders.".format(self.ticker))
Close position of given instruments.
Parameters
- tickers: tickers list of instruments that must be closed.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3069 def CloseAllTrades(self, iType: str, portfolio: dict = None) -> None: 3070 """ 3071 Close all positions of given instruments with defined type. 3072 3073 :param iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list. 3074 :param portfolio: pre-received dictionary with open trades, returned by `Overview()` method. 3075 This avoids unnecessary downloading data from the server. 3076 """ 3077 if iType not in TKS_INSTRUMENTS: 3078 uLogger.warning("Type of the instrument must be one of supported types: {}. Given: [{}]".format(", ".join(TKS_INSTRUMENTS), iType)) 3079 3080 else: 3081 if portfolio is None or not portfolio: 3082 portfolio = self.Overview(show=False) 3083 3084 tickers = [item["ticker"] for item in portfolio["stat"][iType]] 3085 uLogger.debug("Instrument tickers with type [{}] that will be closed: {}".format(iType, tickers)) 3086 3087 if tickers and portfolio: 3088 self.CloseTrades(tickers, portfolio) 3089 3090 else: 3091 uLogger.info("Instrument tickers with type [{}] not found, nothing to close.".format(iType))
Close all positions of given instruments with defined type.
Parameters
- iType: type of the instruments that be closed, it must be one of supported types in TKS_INSTRUMENTS list.
- portfolio: pre-received dictionary with open trades, returned by
Overview()method. This avoids unnecessary downloading data from the server.
3093 def Order(self, operation: str, orderType: str, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3094 """ 3095 Universal method to create market or limit orders with all available parameters for current `accountId`. 3096 See more simple methods: `BuyLimit()`, `BuyStop()`, `SellLimit()`, `SellStop()`. 3097 3098 If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above 3099 current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day. 3100 3101 Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" 3102 then broker immediately open market order as you can do simple --buy or --sell operations! 3103 3104 If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". 3105 When current price will go up or down to target price value then broker opens a limit order. 3106 Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter. 3107 3108 Only one attempt and no retry for opens order. If network issue occurred you can create new request. 3109 3110 :param operation: string "Buy" or "Sell". 3111 :param orderType: string "Limit" or "Stop". 3112 :param lots: volume, integer count of lots >= 1. 3113 :param targetPrice: target price > 0. This is open trade price for limit order. 3114 :param limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. 3115 Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order. 3116 :param stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types 3117 "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3118 Stop loss order always executed by market price. 3119 :param expDate: string "Undefined" by default or local date in future. 3120 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3121 This date is converting to UTC format for server. This parameter only makes sense for stop-order. 3122 A limit order has no expiration date, it lasts until the end of the trading day. 3123 :return: JSON with response from broker server. 3124 """ 3125 if self.accountId is None or not self.accountId: 3126 uLogger.error("Variable `accountId` must be defined for using this method!") 3127 raise Exception("Account ID required") 3128 3129 if operation is None or not operation or operation not in ("Buy", "Sell"): 3130 uLogger.error("You must define operation type only one of them: `Buy` or `Sell`!") 3131 raise Exception("Incorrect value") 3132 3133 if orderType is None or not orderType or orderType not in ("Limit", "Stop"): 3134 uLogger.error("You must define order type only one of them: `Limit` or `Stop`!") 3135 raise Exception("Incorrect value") 3136 3137 if lots is None or lots < 1: 3138 uLogger.error("You must define trade volume > 0: integer count of lots!") 3139 raise Exception("Incorrect value") 3140 3141 if targetPrice is None or targetPrice <= 0: 3142 uLogger.error("Target price for limit-order must be greater than 0!") 3143 raise Exception("Incorrect value") 3144 3145 if limitPrice is None or limitPrice <= 0: 3146 limitPrice = targetPrice 3147 3148 if stopType is None or not stopType or stopType not in ("SL", "TP", "Limit"): 3149 stopType = "Limit" 3150 3151 if expDate is None or not expDate: 3152 expDate = "Undefined" 3153 3154 if not (self.ticker or self.figi): 3155 uLogger.error("Tocker or FIGI must be defined!") 3156 raise Exception("Ticker or FIGI required") 3157 3158 response = {} 3159 instrument = self.SearchByTicker(requestPrice=True, debug=False) if self.ticker else self.SearchByFIGI(requestPrice=True, debug=False) 3160 self.ticker = instrument["ticker"] 3161 self.figi = instrument["figi"] 3162 3163 if orderType == "Limit": 3164 uLogger.debug( 3165 "Creating pending limit-order: ticker [{}], FIGI [{}], action [{}], lots [{}] and the target price [{:.2f} {}]. Wait, please...".format( 3166 self.ticker, self.figi, 3167 operation, lots, targetPrice, instrument["currency"], 3168 )) 3169 3170 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/PostOrder" 3171 self.body = str({ 3172 "figi": self.figi, 3173 "quantity": str(lots), 3174 "price": FloatToNano(targetPrice), 3175 "direction": "ORDER_DIRECTION_BUY" if operation == "Buy" else "ORDER_DIRECTION_SELL", # see: TKS_ORDER_DIRECTIONS 3176 "accountId": str(self.accountId), 3177 "orderType": "ORDER_TYPE_LIMIT", # see: TKS_ORDER_TYPES 3178 }) 3179 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3180 3181 if "orderId" in response.keys(): 3182 uLogger.info( 3183 "Limit-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}]".format( 3184 response["orderId"], 3185 self.ticker, self.figi, 3186 operation, lots, targetPrice, instrument["currency"], 3187 )) 3188 3189 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3190 if operation == "Buy" and targetPrice > instrument["currentPrice"]["lastPrice"]: 3191 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was higher than current price [{:.2f} {}] broker immediately opened `Buy` market order, such as if you did simple `--buy` operation.".format( 3192 targetPrice, instrument["currency"], 3193 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3194 )) 3195 3196 if operation == "Sell" and targetPrice < instrument["currentPrice"]["lastPrice"]: 3197 uLogger.warning("Your order was executed as a market order, not as a limit order! Comment: because your target price [{:.2f} {}] was lower than current price [{:.2f} {}] broker immediately opened `Sell` market order, such as if you did simple `--sell` operation.".format( 3198 targetPrice, instrument["currency"], 3199 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3200 )) 3201 3202 else: 3203 uLogger.warning("Not `oK` status received! Limit order not opened. See full debug log or try again and open order later.") 3204 3205 if orderType == "Stop": 3206 uLogger.debug( 3207 "Creating stop-order: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and local expiration date [{}]. Wait, please...".format( 3208 self.ticker, self.figi, 3209 operation, lots, 3210 targetPrice, instrument["currency"], 3211 limitPrice, instrument["currency"], 3212 stopType, expDate, 3213 )) 3214 3215 openOrderURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/PostStopOrder" 3216 expDateUTC = "" if expDate == "Undefined" else datetime.strptime(expDate, TKS_PRINT_DATE_TIME_FORMAT).replace(tzinfo=tzlocal()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT_EXT) 3217 stopOrderType = "STOP_ORDER_TYPE_STOP_LOSS" if stopType == "SL" else "STOP_ORDER_TYPE_TAKE_PROFIT" if stopType == "TP" else "STOP_ORDER_TYPE_STOP_LIMIT" 3218 3219 body = { 3220 "figi": self.figi, 3221 "quantity": str(lots), 3222 "price": FloatToNano(limitPrice), 3223 "stopPrice": FloatToNano(targetPrice), 3224 "direction": "STOP_ORDER_DIRECTION_BUY" if operation == "Buy" else "STOP_ORDER_DIRECTION_SELL", # see: TKS_STOP_ORDER_DIRECTIONS 3225 "accountId": str(self.accountId), 3226 "expirationType": "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_DATE" if expDateUTC else "STOP_ORDER_EXPIRATION_TYPE_GOOD_TILL_CANCEL", # see: TKS_STOP_ORDER_EXPIRATION_TYPES 3227 "stopOrderType": stopOrderType, # see: TKS_STOP_ORDER_TYPES 3228 } 3229 3230 if expDateUTC: 3231 body["expireDate"] = expDateUTC 3232 3233 self.body = str(body) 3234 response = self.SendAPIRequest(openOrderURL, reqType="POST", retry=0, debug=False) 3235 3236 if "stopOrderId" in response.keys(): 3237 uLogger.info( 3238 "Stop-order [{}] was created: ticker [{}], FIGI [{}], action [{}], lots [{}], target price [{:.2f} {}], limit price [{:.2f} {}], stop-order type [{}] and expiration date in UTC [{}]".format( 3239 response["stopOrderId"], 3240 self.ticker, self.figi, 3241 operation, lots, 3242 targetPrice, instrument["currency"], 3243 limitPrice, instrument["currency"], 3244 TKS_STOP_ORDER_TYPES[stopOrderType], 3245 datetime.strptime(expDateUTC, TKS_DATE_TIME_FORMAT_EXT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if expDateUTC else TKS_STOP_ORDER_EXPIRATION_TYPES["STOP_ORDER_EXPIRATION_TYPE_UNSPECIFIED"], 3246 )) 3247 3248 if "lastPrice" in instrument["currentPrice"].keys() and instrument["currentPrice"]["lastPrice"]: 3249 if operation == "Buy" and targetPrice < instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3250 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target buy price [{:.2f} {}] is lower than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3251 targetPrice, instrument["currency"], 3252 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3253 )) 3254 3255 if operation == "Sell" and targetPrice > instrument["currentPrice"]["lastPrice"] and stopType != "TP": 3256 uLogger.warning("The broker will cancel this order after some time. Comment: you placed the wrong stop order because the target sell price [{:.2f} {}] is higher than the current price [{:.2f} {}]. Also try to set up order type as `TP` if you want to place stop order at that price.".format( 3257 targetPrice, instrument["currency"], 3258 instrument["currentPrice"]["lastPrice"], instrument["currency"], 3259 )) 3260 3261 else: 3262 uLogger.warning("Not `oK` status received! Stop order not opened. See full debug log or try again and open order later.") 3263 3264 return response
Universal method to create market or limit orders with all available parameters for current accountId.
See more simple methods: BuyLimit(), BuyStop(), SellLimit(), SellStop().
If orderType is "Limit" then create pending limit-order below current price if operation is "Buy" and above current price if operation is "Sell". A limit order has no expiration date, it lasts until the end of the trading day.
Warning! If you try to create limit-order above current price if "Buy" or below current price if "Sell" then broker immediately open market order as you can do simple --buy or --sell operations!
If orderType is "Stop" then creates stop-order with any direction "Buy" or "Sell". When current price will go up or down to target price value then broker opens a limit order. Stop-order is opened with unlimited expiration date by default, or you can define expiration date with expDate parameter.
Only one attempt and no retry for opens order. If network issue occurred you can create new request.
Parameters
- operation: string "Buy" or "Sell".
- orderType: string "Limit" or "Stop".
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
- limitPrice: limit price >= 0. This parameter only makes sense for stop-order. If limitPrice = 0, then it set as targetPrice. Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of stop-order.
- stopType: string "Limit" by default. This parameter only makes sense for stop-order. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly. Stop loss order always executed by market price.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server. This parameter only makes sense for stop-order. A limit order has no expiration date, it lasts until the end of the trading day.
Returns
JSON with response from broker server.
3266 def BuyLimit(self, lots: int, targetPrice: float) -> dict: 3267 """ 3268 Create pending `Buy` limit-order (below current price). You must specify only 2 parameters: 3269 `lots` and `target price` to open buy limit-order. If you try to create buy limit-order above current price then 3270 broker immediately open `Buy` market order, such as if you do simple `--buy` operation! 3271 See also: `Order()` docstring. 3272 3273 :param lots: volume, integer count of lots >= 1. 3274 :param targetPrice: target price > 0. This is open trade price for limit order. 3275 :return: JSON with response from broker server. 3276 """ 3277 return self.Order(operation="Buy", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Buy limit-order (below current price). You must specify only 2 parameters:
lots and target price to open buy limit-order. If you try to create buy limit-order above current price then
broker immediately open Buy market order, such as if you do simple --buy operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3279 def BuyStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3280 """ 3281 Create `Buy` stop-order. You must specify at least 2 parameters: `lots` `target price` to open buy stop-order. 3282 In additional you can specify 3 parameters for buy stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3283 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3284 target price value then broker opens a limit order. See also: `Order()` docstring. 3285 3286 :param lots: volume, integer count of lots >= 1. 3287 :param targetPrice: target price > 0. This is trigger price for buy stop-order. 3288 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3289 with price equal to limitPrice, when current price goes to target price of buy stop-order. 3290 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3291 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3292 :param expDate: string "Undefined" by default or local date in future. 3293 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3294 This date is converting to UTC format for server. 3295 :return: JSON with response from broker server. 3296 """ 3297 return self.Order(operation="Buy", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Buy stop-order. You must specify at least 2 parameters: lots target price to open buy stop-order.
In additional you can specify 3 parameters for buy stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for buy stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of buy stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3299 def SellLimit(self, lots: int, targetPrice: float) -> dict: 3300 """ 3301 Create pending `Sell` limit-order (above current price). You must specify only 2 parameters: 3302 `lots` and `target price` to open sell limit-order. If you try to create sell limit-order below current price then 3303 broker immediately open `Sell` market order, such as if you do simple `--sell` operation! 3304 See also: `Order()` docstring. 3305 3306 :param lots: volume, integer count of lots >= 1. 3307 :param targetPrice: target price > 0. This is open trade price for limit order. 3308 :return: JSON with response from broker server. 3309 """ 3310 return self.Order(operation="Sell", orderType="Limit", lots=lots, targetPrice=targetPrice)
Create pending Sell limit-order (above current price). You must specify only 2 parameters:
lots and target price to open sell limit-order. If you try to create sell limit-order below current price then
broker immediately open Sell market order, such as if you do simple --sell operation!
See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is open trade price for limit order.
Returns
JSON with response from broker server.
3312 def SellStop(self, lots: int, targetPrice: float, limitPrice: float = 0., stopType: str = "Limit", expDate: str = "Undefined") -> dict: 3313 """ 3314 Create `Sell` stop-order. You must specify at least 2 parameters: `lots` `target price` to open sell stop-order. 3315 In additional you can specify 3 parameters for sell stop-order: `limit price` >=0, `stop type` = Limit|SL|TP, 3316 `expiration date` = Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`. When current price will go up or down to 3317 target price value then broker opens a limit order. See also: `Order()` docstring. 3318 3319 :param lots: volume, integer count of lots >= 1. 3320 :param targetPrice: target price > 0. This is trigger price for sell stop-order. 3321 :param limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order 3322 with price equal to limitPrice, when current price goes to target price of sell stop-order. 3323 :param stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" 3324 for "Stop loss", "Take profit" and "Stop limit" types accordingly. 3325 :param expDate: string "Undefined" by default or local date in future. 3326 String has a format like this: `%Y-%m-%d %H:%M:%S`. 3327 This date is converting to UTC format for server. 3328 :return: JSON with response from broker server. 3329 """ 3330 return self.Order(operation="Sell", orderType="Stop", lots=lots, targetPrice=targetPrice, limitPrice=limitPrice, stopType=stopType, expDate=expDate)
Create Sell stop-order. You must specify at least 2 parameters: lots target price to open sell stop-order.
In additional you can specify 3 parameters for sell stop-order: limit price >=0, stop type = Limit|SL|TP,
expiration date = Undefined|%%Y-%%m-%%d %%H:%%M:%%S. When current price will go up or down to
target price value then broker opens a limit order. See also: Order() docstring.
Parameters
- lots: volume, integer count of lots >= 1.
- targetPrice: target price > 0. This is trigger price for sell stop-order.
- limitPrice: limit price >= 0 (limitPrice = targetPrice if limitPrice is 0). Broker will creates limit-order with price equal to limitPrice, when current price goes to target price of sell stop-order.
- stopType: string "Limit" by default. There are 3 stop-order types "SL", "TP", "Limit" for "Stop loss", "Take profit" and "Stop limit" types accordingly.
- expDate: string "Undefined" by default or local date in future.
String has a format like this:
%Y-%m-%d %H:%M:%S. This date is converting to UTC format for server.
Returns
JSON with response from broker server.
3332 def CloseOrders(self, orderIDs: list, allOrdersIDs: list = None, allStopOrdersIDs: list = None) -> None: 3333 """ 3334 Cancel order or list of orders by its `orderId` or `stopOrderId` for current `accountId`. 3335 3336 :param orderIDs: list of integers with `orderId` or `stopOrderId`. 3337 :param allOrdersIDs: pre-received lists of all active pending orders. 3338 This avoids unnecessary downloading data from the server. 3339 :param allStopOrdersIDs: pre-received lists of all active stop orders. 3340 """ 3341 if self.accountId is None or not self.accountId: 3342 uLogger.error("Variable `accountId` must be defined for using this method!") 3343 raise Exception("Account ID required") 3344 3345 if orderIDs: 3346 if allOrdersIDs is None or not allOrdersIDs: 3347 rawOrders = self.RequestPendingOrders() 3348 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3349 3350 if allStopOrdersIDs is None or not allStopOrdersIDs: 3351 rawStopOrders = self.RequestStopOrders() 3352 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3353 3354 for orderID in orderIDs: 3355 idInPendingOrders = orderID in allOrdersIDs 3356 idInStopOrders = orderID in allStopOrdersIDs 3357 3358 if not (idInPendingOrders or idInStopOrders): 3359 uLogger.warning("Order not found by ID: [{}]. Maybe cancelled already? Check it with `--overview` key.".format(orderID)) 3360 continue 3361 3362 else: 3363 if idInPendingOrders: 3364 uLogger.debug("Cancelling pending order with ID: [{}]. Wait, please...".format(orderID)) 3365 3366 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/OrdersService/OrdersService_CancelOrder 3367 self.body = str({"accountId": self.accountId, "orderId": orderID}) 3368 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OrdersService/CancelOrder" 3369 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3370 3371 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3372 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3373 uLogger.info("Pending order with ID [{}] successfully cancel".format(orderID)) 3374 3375 else: 3376 uLogger.warning("Unknown issue occurred when cancelling pending order with ID: [{}]. Check ID and try again.".format(orderID)) 3377 3378 elif idInStopOrders: 3379 uLogger.debug("Cancelling stop order with ID: [{}]. Wait, please...".format(orderID)) 3380 3381 # REST API for request: https://tinkoff.github.io/investAPI/swagger-ui/#/StopOrdersService/StopOrdersService_CancelStopOrder 3382 self.body = str({"accountId": self.accountId, "stopOrderId": orderID}) 3383 closeURL = self.server + r"/tinkoff.public.invest.api.contract.v1.StopOrdersService/CancelStopOrder" 3384 responseJSON = self.SendAPIRequest(closeURL, reqType="POST") 3385 3386 if responseJSON and "time" in responseJSON.keys() and responseJSON["time"]: 3387 uLogger.debug("Success time marker received from server: [{}] (UTC)".format(responseJSON["time"])) 3388 uLogger.info("Stop order with ID [{}] successfully cancel".format(orderID)) 3389 3390 else: 3391 uLogger.warning("Unknown issue occurred when cancelling stop order with ID: [{}]. Check ID and try again.".format(orderID)) 3392 3393 else: 3394 continue
Cancel order or list of orders by its orderId or stopOrderId for current accountId.
Parameters
- orderIDs: list of integers with
orderIdorstopOrderId. - allOrdersIDs: pre-received lists of all active pending orders. This avoids unnecessary downloading data from the server.
- allStopOrdersIDs: pre-received lists of all active stop orders.
3396 def CloseAllOrders(self) -> None: 3397 """ 3398 Gets a list of open pending and stop orders and cancel it all. 3399 """ 3400 rawOrders = self.RequestPendingOrders() 3401 allOrdersIDs = [item["orderId"] for item in rawOrders] # all pending orders ID 3402 lenOrders = len(allOrdersIDs) 3403 3404 rawStopOrders = self.RequestStopOrders() 3405 allStopOrdersIDs = [item["stopOrderId"] for item in rawStopOrders] # all stop orders ID 3406 lenSOrders = len(allStopOrdersIDs) 3407 3408 if lenOrders > 0 or lenSOrders > 0: 3409 uLogger.info("Found: [{}] opened pending and [{}] stop orders. Let's trying to cancel it all. Wait, please...".format(lenOrders, lenSOrders)) 3410 3411 self.CloseOrders(allOrdersIDs + allStopOrdersIDs, allOrdersIDs, allStopOrdersIDs) 3412 3413 else: 3414 uLogger.info("Orders not found, nothing to cancel.")
Gets a list of open pending and stop orders and cancel it all.
3416 def CloseAll(self, *args) -> None: 3417 """ 3418 Close all available (not blocked) opened trades and orders. 3419 3420 Also, you can select one or more keywords case-insensitive: 3421 `orders`, `shares`, `bonds`, `etfs` and `futures` from `TKS_INSTRUMENTS` enum to specify trades type. 3422 3423 Currency positions you must close manually using buy or sell operations, `CloseTrades()` or `CloseAllTrades()` methods. 3424 """ 3425 overview = self.Overview(show=False) # get all open trades info 3426 3427 if len(args) == 0: 3428 uLogger.debug("Closing all available (not blocked) opened trades and orders. Currency positions you must closes manually using buy or sell operations! Wait, please...") 3429 self.CloseAllOrders() # close all pending and stop orders 3430 3431 for iType in TKS_INSTRUMENTS: 3432 if iType != "Currencies": 3433 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies 3434 3435 else: 3436 uLogger.debug("Closing all available {}. Currency positions you must closes manually using buy or sell operations! Wait, please...".format(list(args))) 3437 lowerArgs = [x.lower() for x in args] 3438 3439 if "orders" in lowerArgs: 3440 self.CloseAllOrders() # close all pending and stop orders 3441 3442 for iType in TKS_INSTRUMENTS: 3443 if iType.lower() in lowerArgs and iType != "Currencies": 3444 self.CloseAllTrades(iType, overview) # close all positions of instruments with same type without currencies
Close all available (not blocked) opened trades and orders.
Also, you can select one or more keywords case-insensitive:
orders, shares, bonds, etfs and futures from TKS_INSTRUMENTS enum to specify trades type.
Currency positions you must close manually using buy or sell operations, CloseTrades() or CloseAllTrades() methods.
3446 @staticmethod 3447 def ParseOrderParameters(operation, **inputParameters): 3448 """ 3449 Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders. 3450 3451 :param operation: string "Buy" or "Sell". 3452 :param inputParameters: this is dict of strings that looks like this 3453 `{"lots": "L_int,...", "prices": "P_float,..."}` where 3454 "lots" key: one or more lot values (integer numbers) to open with every limit-order 3455 "prices" key: one or more prices to open limit-orders 3456 Counts of values in lots and prices lists must be equals! 3457 :return: list of dictionaries with all lots and prices to open orders that looks like this `[{"lot": lots_1, "price": price_1}, {...}, ...]` 3458 """ 3459 # TODO: update order grid work with api v2 3460 pass 3461 # uLogger.debug("Input parameters: {}".format(inputParameters)) 3462 # 3463 # if operation is None or not operation or operation not in ("Buy", "Sell"): 3464 # uLogger.error("You must define operation type: 'Buy' or 'Sell'!") 3465 # raise Exception("Incorrect value") 3466 # 3467 # if "l" in inputParameters.keys(): 3468 # inputParameters["lots"] = inputParameters.pop("l") 3469 # 3470 # if "p" in inputParameters.keys(): 3471 # inputParameters["prices"] = inputParameters.pop("p") 3472 # 3473 # if "lots" not in inputParameters.keys() or "prices" not in inputParameters.keys(): 3474 # uLogger.error("Both of 'lots' and 'prices' keys must be define to open grid orders!") 3475 # raise Exception("Incorrect value") 3476 # 3477 # lots = [int(item.strip()) for item in inputParameters["lots"].split(",")] 3478 # prices = [float(item.strip()) for item in inputParameters["prices"].split(",")] 3479 # 3480 # if len(lots) != len(prices): 3481 # uLogger.error("'lots' and 'prices' lists must have equal length of values!") 3482 # raise Exception("Incorrect value") 3483 # 3484 # uLogger.debug("Extracted parameters for orders:") 3485 # uLogger.debug("lots = {}".format(lots)) 3486 # uLogger.debug("prices = {}".format(prices)) 3487 # 3488 # # list of dictionaries with order's parameters: [{"lot": lots_1, "price": price_1}, {...}, ...] 3489 # result = [{"lot": lots[item], "price": prices[item]} for item in range(len(prices))] 3490 # uLogger.debug("Order parameters: {}".format(result)) 3491 # 3492 # return result
Parse input dictionary of strings with order parameters and return dictionary with parameters to open all orders.
Parameters
- operation: string "Buy" or "Sell".
- inputParameters: this is dict of strings that looks like this
{"lots": "L_int,...", "prices": "P_float,..."}where "lots" key: one or more lot values (integer numbers) to open with every limit-order "prices" key: one or more prices to open limit-orders Counts of values in lots and prices lists must be equals!
Returns
list of dictionaries with all lots and prices to open orders that looks like this
[{"lot": lots_1, "price": price_1}, {...}, ...]
3494 def IsInPortfolio(self, portfolio: dict = None) -> bool: 3495 """ 3496 Checks if instrument is in the user's portfolio. Instrument must be defined by `ticker` (highly priority) or `figi`. 3497 3498 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3499 :return: `True` if portfolio contains open position with given instrument, `False` otherwise. 3500 """ 3501 result = False 3502 msg = "Instrument not defined!" 3503 3504 if portfolio is None or not portfolio: 3505 portfolio = self.Overview(show=False) 3506 3507 if self.ticker: 3508 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3509 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3510 3511 for iType in TKS_INSTRUMENTS: 3512 for instrument in portfolio["stat"][iType]: 3513 if instrument["ticker"] == self.ticker: 3514 result = True 3515 msg = "Instrument with ticker [{}] is present in open positions".format(self.ticker) 3516 break 3517 3518 elif self.figi: 3519 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3520 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3521 3522 for iType in TKS_INSTRUMENTS: 3523 for instrument in portfolio["stat"][iType]: 3524 if instrument["figi"] == self.figi: 3525 result = True 3526 msg = "Instrument with FIGI [{}] is present in open positions".format(self.figi) 3527 break 3528 3529 else: 3530 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3531 3532 uLogger.debug(msg) 3533 3534 return result
Checks if instrument is in the user's portfolio. Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
Trueif portfolio contains open position with given instrument,Falseotherwise.
3536 def GetInstrumentFromPortfolio(self, portfolio: dict = None) -> dict: 3537 """ 3538 Returns instrument is in the user's portfolio if it presents there. 3539 Instrument must be defined by `ticker` (highly priority) or `figi`. 3540 3541 :param portfolio: dict with user's portfolio data. If `None`, then requests portfolio from `Overview()` method. 3542 :return: dict with instrument if portfolio contains open position with this instrument, `None` otherwise. 3543 """ 3544 result = None 3545 msg = "Instrument not defined!" 3546 3547 if portfolio is None or not portfolio: 3548 portfolio = self.Overview(show=False) 3549 3550 if self.ticker: 3551 uLogger.debug("Searching instrument with ticker [{}] throwout opened positions...".format(self.ticker)) 3552 msg = "Instrument with ticker [{}] is not present in open positions".format(self.ticker) 3553 3554 for iType in TKS_INSTRUMENTS: 3555 for instrument in portfolio["stat"][iType]: 3556 if instrument["ticker"] == self.ticker: 3557 result = instrument 3558 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(self.ticker, instrument["figi"]) 3559 break 3560 3561 elif self.figi: 3562 uLogger.debug("Searching instrument with FIGI [{}] throwout opened positions...".format(self.figi)) 3563 msg = "Instrument with FIGI [{}] is not present in open positions".format(self.figi) 3564 3565 for iType in TKS_INSTRUMENTS: 3566 for instrument in portfolio["stat"][iType]: 3567 if instrument["figi"] == self.figi: 3568 result = instrument 3569 msg = "Instrument with ticker [{}] and FIGI [{}] is present in open positions".format(instrument["ticker"], self.figi) 3570 break 3571 3572 else: 3573 uLogger.warning("Instrument must be defined by `ticker` (highly priority) or `figi`!") 3574 3575 uLogger.debug(msg) 3576 3577 return result
Returns instrument is in the user's portfolio if it presents there.
Instrument must be defined by ticker (highly priority) or figi.
Parameters
- portfolio: dict with user's portfolio data. If
None, then requests portfolio fromOverview()method.
Returns
dict with instrument if portfolio contains open position with this instrument,
Noneotherwise.
3579 def RequestLimits(self) -> dict: 3580 """ 3581 Method for obtaining the available funds for withdrawal for current `accountId`. 3582 3583 See also: 3584 - REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits 3585 - `OverviewLimits()` method 3586 3587 :return: dict with raw data from server that contains free funds for withdrawal. Example of dict: 3588 `{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}`. 3589 Here `money` is an array of portfolio currency positions, `blocked` is an array of blocked currency 3590 positions of the portfolio and `blockedGuarantee` is locked money under collateral for futures. 3591 """ 3592 if self.accountId is None or not self.accountId: 3593 uLogger.error("Variable `accountId` must be defined for using this method!") 3594 raise Exception("Account ID required") 3595 3596 uLogger.debug("Requesting current available funds for withdrawal. Wait, please...") 3597 3598 self.body = str({"accountId": self.accountId}) 3599 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.OperationsService/GetWithdrawLimits" 3600 rawLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3601 3602 uLogger.debug("Records about available funds for withdrawal successfully received") 3603 3604 return rawLimits
Method for obtaining the available funds for withdrawal for current accountId.
See also:
- REST API for limits: https://tinkoff.github.io/investAPI/swagger-ui/#/OperationsService/OperationsService_GetWithdrawLimits
OverviewLimits()method
Returns
dict with raw data from server that contains free funds for withdrawal. Example of dict:
{"money": [{"currency": "rub", "units": "100", "nano": 290000000}, {...}], "blocked": [...], "blockedGuarantee": [...]}. Heremoneyis an array of portfolio currency positions,blockedis an array of blocked currency positions of the portfolio andblockedGuaranteeis locked money under collateral for futures.
3606 def OverviewLimits(self, show: bool = False) -> dict: 3607 """ 3608 Method for parsing and show table with available funds for withdrawal for current `accountId`. 3609 3610 See also: `RequestLimits()`. 3611 3612 :param show: if `False` then only dictionary returns, if `True` then also print withdrawal limits to log. 3613 :return: dict with raw parsed data from server and some calculated statistics about it. 3614 """ 3615 if self.accountId is None or not self.accountId: 3616 uLogger.error("Variable `accountId` must be defined for using this method!") 3617 raise Exception("Account ID required") 3618 3619 rawLimits = self.RequestLimits() # raw response with current available funds for withdrawal 3620 3621 view = { 3622 "rawLimits": rawLimits, 3623 "limits": { # parsed data for every currency: 3624 "money": { # this is an array of portfolio currency positions 3625 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["money"] 3626 }, 3627 "blocked": { # this is an array of blocked currency 3628 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blocked"] 3629 }, 3630 "blockedGuarantee": { # this is locked money under collateral for futures 3631 item["currency"]: NanoToFloat(item["units"], item["nano"]) for item in rawLimits["blockedGuarantee"] 3632 }, 3633 }, 3634 } 3635 3636 # --- Prepare text table with limits in human-readable format: 3637 if show: 3638 info = [ 3639 "# Withdrawal limits\n\n", 3640 "* **Actual date:** [{} UTC]\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 3641 "* **Account ID:** [{}]\n".format(self.accountId), 3642 "\n| Currencies | Total | Available for withdrawal | Blocked for trade | Futures guarantee |\n", 3643 "|------------|---------------|--------------------------|-------------------|-------------------|\n", 3644 ] 3645 3646 for curr in view["limits"]["money"].keys(): 3647 blocked = view["limits"]["blocked"][curr] if curr in view["limits"]["blocked"].keys() else 0 3648 blockedGuarantee = view["limits"]["blockedGuarantee"][curr] if curr in view["limits"]["blockedGuarantee"].keys() else 0 3649 availableMoney = view["limits"]["money"][curr] - (blocked + blockedGuarantee) 3650 3651 infoStr = "| {:<10} | {:<13} | {:<24} | {:<17} | {:<17} |\n".format( 3652 "[{}]".format(curr), 3653 "{:.2f}".format(view["limits"]["money"][curr]), 3654 "{:.2f}".format(availableMoney), 3655 "{:.2f}".format(view["limits"]["blocked"][curr]) if curr in view["limits"]["blocked"].keys() else "—", 3656 "{:.2f}".format(view["limits"]["blockedGuarantee"][curr]) if curr in view["limits"]["blockedGuarantee"].keys() else "—", 3657 ) 3658 3659 if curr == "rub": 3660 info.insert(5, infoStr) # insert at first position in table and after headers 3661 3662 else: 3663 info.append(infoStr) 3664 3665 infoText = "".join(info) 3666 3667 uLogger.info(infoText) 3668 3669 if self.withdrawalLimitsFile: 3670 with open(self.withdrawalLimitsFile, "w", encoding="UTF-8") as fH: 3671 fH.write(infoText) 3672 3673 uLogger.info("Client's withdrawal limits was saved to file: [{}]".format(os.path.abspath(self.withdrawalLimitsFile))) 3674 3675 return view
Method for parsing and show table with available funds for withdrawal for current accountId.
See also: RequestLimits().
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print withdrawal limits to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
3677 def RequestAccounts(self) -> dict: 3678 """ 3679 Method for requesting all brokerage accounts (`accountId`s) of current user detected by `token`. 3680 3681 See also: 3682 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts 3683 - What does account fields mean: https://tinkoff.github.io/investAPI/users/#account 3684 - `OverviewUserInfo()` method 3685 3686 :return: dict with raw data from server that contains accounts info. Example of dict: 3687 `{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", 3688 "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", 3689 "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}`. 3690 If `closedDate="1970-01-01T00:00:00Z"` it means that account is active now. 3691 """ 3692 uLogger.debug("Requesting all brokerage accounts of current user detected by its token. Wait, please...") 3693 3694 self.body = str({}) 3695 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetAccounts" 3696 rawAccounts = self.SendAPIRequest(portfolioURL, reqType="POST") 3697 3698 uLogger.debug("Records about available accounts successfully received") 3699 3700 return rawAccounts
Method for requesting all brokerage accounts (accountIds) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetAccounts
- What does account fields mean: https://tinkoff.github.io/investAPI/users/#account
OverviewUserInfo()method
Returns
dict with raw data from server that contains accounts info. Example of dict:
{"accounts": [{"id": "20000xxxxx", "type": "ACCOUNT_TYPE_TINKOFF", "name": "TKSBrokerAPI account", "status": "ACCOUNT_STATUS_OPEN", "openedDate": "2018-05-23T00:00:00Z", "closedDate": "1970-01-01T00:00:00Z", "accessLevel": "ACCOUNT_ACCESS_LEVEL_FULL_ACCESS"}, ...]}. IfclosedDate="1970-01-01T00:00:00Z"it means that account is active now.
3702 def RequestUserInfo(self) -> dict: 3703 """ 3704 Method for requesting common user's information. 3705 3706 See also: 3707 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo 3708 - What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest 3709 - What does `qualified_for_work_with` field mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with 3710 - `OverviewUserInfo()` method 3711 3712 :return: dict with raw data from server that contains user's information. Example of dict: 3713 `{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", 3714 "russian_shares", "structured_income_bonds"], "tariff": "premium"}`. 3715 """ 3716 uLogger.debug("Requesting common user's information. Wait, please...") 3717 3718 self.body = str({}) 3719 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetInfo" 3720 rawUserInfo = self.SendAPIRequest(portfolioURL, reqType="POST") 3721 3722 uLogger.debug("Records about current user successfully received") 3723 3724 return rawUserInfo
Method for requesting common user's information.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetInfo
- What does user info fields mean: https://tinkoff.github.io/investAPI/users/#getinforequest
- What does
qualified_for_work_withfield mean: https://tinkoff.github.io/investAPI/faq_users/#qualified_for_work_with OverviewUserInfo()method
Returns
dict with raw data from server that contains user's information. Example of dict:
{"premStatus": true, "qualStatus": false, "qualifiedForWorkWith": ["bond", "foreign_shares", "leverage", "russian_shares", "structured_income_bonds"], "tariff": "premium"}.
3726 def RequestMarginStatus(self, accountId: str = None) -> dict: 3727 """ 3728 Method for requesting margin calculation for defined account ID. 3729 3730 See also: 3731 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes 3732 - What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse 3733 - `OverviewUserInfo()` method 3734 3735 :param accountId: string with numeric account ID. If `None`, then used class field `accountId`. 3736 :return: dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. 3737 Example of responses: 3738 status code 400: `{"code": 3, "message": "account margin status is disabled", "description": "30051" }`, returns: `{}`. 3739 status code 200: `{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, 3740 "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, 3741 "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, 3742 "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, 3743 "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}`. 3744 """ 3745 if accountId is None or not accountId: 3746 if self.accountId is None or not self.accountId: 3747 uLogger.error("Variable `accountId` must be defined for using this method!") 3748 raise Exception("Account ID required") 3749 3750 else: 3751 accountId = self.accountId # use `self.accountId` (main ID) by default 3752 3753 uLogger.debug("Requesting margin calculation for accountId [{}]. Wait, please...".format(accountId)) 3754 3755 self.body = str({"accountId": accountId}) 3756 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetMarginAttributes" 3757 rawMargin = self.SendAPIRequest(portfolioURL, reqType="POST") 3758 3759 if rawMargin == {"code": 3, "message": "account margin status is disabled", "description": "30051"}: 3760 uLogger.debug("Server response: margin status is disabled for current accountId [{}]".format(accountId)) 3761 rawMargin = {} 3762 3763 else: 3764 uLogger.debug("Records with margin calculation for accountId [{}] successfully received".format(accountId)) 3765 3766 return rawMargin
Method for requesting margin calculation for defined account ID.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetMarginAttributes
- What does margin fields mean: https://tinkoff.github.io/investAPI/users/#getmarginattributesresponse
OverviewUserInfo()method
Parameters
- accountId: string with numeric account ID. If
None, then used class fieldaccountId.
Returns
dict with raw data from server that contains margin calculation. If margin is disabled then returns empty dict. Example of responses: status code 400:
{"code": 3, "message": "account margin status is disabled", "description": "30051" }, returns:{}. status code 200:{"liquidPortfolio": {"currency": "rub", "units": "7175", "nano": 560000000}, "startingMargin": {"currency": "rub", "units": "6311", "nano": 840000000}, "minimalMargin": {"currency": "rub", "units": "3155", "nano": 920000000}, "fundsSufficiencyLevel": {"units": "1", "nano": 280000000}, "amountOfMissingFunds": {"currency": "rub", "units": "-863", "nano": -720000000}}.
3768 def RequestTariffLimits(self) -> dict: 3769 """ 3770 Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by `token`. 3771 3772 See also: 3773 - REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff 3774 - What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest 3775 - Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit 3776 - Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit 3777 - `OverviewUserInfo()` method 3778 3779 :return: dict with raw data from server that contains limits of current tariff. Example of dict: 3780 `{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], 3781 "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}`. 3782 """ 3783 uLogger.debug("Requesting limits of current tariff. Wait, please...") 3784 3785 self.body = str({}) 3786 portfolioURL = self.server + r"/tinkoff.public.invest.api.contract.v1.UsersService/GetUserTariff" 3787 rawTariffLimits = self.SendAPIRequest(portfolioURL, reqType="POST") 3788 3789 uLogger.debug("Records with limits of current tariff successfully received") 3790 3791 return rawTariffLimits
Method for requesting limits of current tariff (connections, API methods etc.) of current user detected by token.
See also:
- REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/UsersService/UsersService_GetUserTariff
- What does fields in tariff mean: https://tinkoff.github.io/investAPI/users/#getusertariffrequest
- Unary limit: https://tinkoff.github.io/investAPI/users/#unarylimit
- Stream limit: https://tinkoff.github.io/investAPI/users/#streamlimit
OverviewUserInfo()method
Returns
dict with raw data from server that contains limits of current tariff. Example of dict:
{"unaryLimits": [{"limitPerMinute": 0, "methods": ["methods", "methods"]}, ...], "streamLimits": [{"streams": ["streams", "streams"], "limit": 6}, ...]}.
3793 def RequestBondCoupons(self, iJSON: dict) -> dict: 3794 """ 3795 Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown 3796 then requesting dates `"from": "1970-01-01T00:00:00.000Z"` and `"to": "2099-12-31T23:59:59.000Z"`. 3797 All dates are in UTC timezone. 3798 3799 REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons 3800 Documentation: 3801 - request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest 3802 - response: https://tinkoff.github.io/investAPI/instruments/#coupon 3803 3804 See also: `ExtendBondsData()`. 3805 3806 :param iJSON: raw json data of a bond from broker server, example `iJSON = self.iList["Bonds"][self.ticker]` 3807 If raw iJSON is not data of bond then server returns an error [400] with message: 3808 `{"code": 3, "message": "instrument type is not bond", "description": "30048"}`. 3809 :return: dictionary with bond payment calendar. Response example 3810 `{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", 3811 "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, 3812 "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", 3813 "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}` 3814 """ 3815 if iJSON["figi"] is None or not iJSON["figi"]: 3816 uLogger.error("FIGI must be defined for using this method!") 3817 raise Exception("FIGI required") 3818 3819 startDate = iJSON["placementDate"] if "placementDate" in iJSON.keys() else "1970-01-01T00:00:00.000Z" 3820 endDate = iJSON["maturityDate"] if "maturityDate" in iJSON.keys() else "2099-12-31T23:59:59.000Z" 3821 3822 uLogger.debug("Requesting bond payment calendar, {}FIGI: [{}], from: [{}], to: [{}]. Wait, please...".format( 3823 "ticker: [{}], ".format(iJSON["ticker"]) if "ticker" in iJSON.keys() else "", 3824 self.figi, 3825 startDate, 3826 endDate, 3827 )) 3828 3829 self.body = str({"figi": iJSON["figi"], "from": startDate, "to": endDate}) 3830 calendarURL = self.server + r"/tinkoff.public.invest.api.contract.v1.InstrumentsService/GetBondCoupons" 3831 calendar = self.SendAPIRequest(calendarURL, reqType="POST", debug=False) 3832 3833 if calendar == {"code": 3, "message": "instrument type is not bond", "description": "30048"}: 3834 uLogger.warning("Instrument type is not bond!") 3835 3836 else: 3837 uLogger.debug("Records about bond payment calendar successfully received") 3838 3839 return calendar
Requesting bond payment calendar from official placement date to maturity date. If these dates are unknown
then requesting dates "from": "1970-01-01T00:00:00.000Z" and "to": "2099-12-31T23:59:59.000Z".
All dates are in UTC timezone.
REST API: https://tinkoff.github.io/investAPI/swagger-ui/#/InstrumentsService/InstrumentsService_GetBondCoupons Documentation:
- request: https://tinkoff.github.io/investAPI/instruments/#getbondcouponsrequest
- response: https://tinkoff.github.io/investAPI/instruments/#coupon
See also: ExtendBondsData().
Parameters
- iJSON: raw json data of a bond from broker server, example
iJSON = self.iList["Bonds"][self.ticker]If raw iJSON is not data of bond then server returns an error [400] with message:{"code": 3, "message": "instrument type is not bond", "description": "30048"}.
Returns
dictionary with bond payment calendar. Response example
{"events": [{"figi": "TCS00A101YV8", "couponDate": "2023-07-26T00:00:00Z", "couponNumber": "12", "fixDate": "2023-07-25T00:00:00Z", "payOneBond": {"currency": "rub", "units": "7", "nano": 170000000}, "couponType": "COUPON_TYPE_CONSTANT", "couponStartDate": "2023-04-26T00:00:00Z", "couponEndDate": "2023-07-26T00:00:00Z", "couponPeriod": 91}, {...}, ...]}
3841 def ExtendBondsData(self, instruments: list[str], xlsx: bool = False) -> pd.DataFrame: 3842 """ 3843 Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider 3844 Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, 3845 coupon yields, current yields and some statistics etc. 3846 3847 WARNING! This is too long operation if a lot of bonds requested from broker server. 3848 3849 See also: `ShowInstrumentInfo()`, `CreateBondsCalendar()`, `ShowBondsCalendar()`, `RequestBondCoupons()`. 3850 3851 :param instruments: list of strings with tickers or FIGIs. 3852 :param xlsx: if True then also exports Pandas DataFrame to xlsx-file `bondsXLSXFile`, default `ext-bonds.xlsx`, 3853 for further used by data scientists or stock analytics. 3854 :return: wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. 3855 In XLSX-file and Pandas DataFrame fields mean: 3856 - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond 3857 - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon 3858 """ 3859 if instruments is None or not instruments: 3860 uLogger.error("List of tickers or FIGIs must be defined for using this method!") 3861 raise Exception("Ticker or FIGI required") 3862 3863 if isinstance(instruments, str): 3864 instruments = [instruments] 3865 3866 uniqueInstruments = self.GetUniqueFIGIs(instruments) 3867 3868 uLogger.debug("Requesting raw bonds calendar from server, transforming and extending it. Wait, please...") 3869 3870 iCount = len(uniqueInstruments) 3871 tooLong = iCount >= 20 3872 if tooLong: 3873 uLogger.warning("You requested a lot of bonds! Operation will takes more time. Wait, please...") 3874 3875 bonds = None 3876 for i, self.figi in enumerate(uniqueInstruments): 3877 instrument = self.SearchByFIGI(requestPrice=False) # raw data about instrument from server 3878 3879 if "type" in instrument.keys() and instrument["type"] == "Bonds": 3880 # raw bond data from server where fields mean: https://tinkoff.github.io/investAPI/instruments/#bond 3881 rawBond = self.SearchByFIGI(requestPrice=True) 3882 3883 # Widen raw data with UTC current time (iData["actualDateTime"]): 3884 actualDate = datetime.now(tzutc()) 3885 iData = {"actualDateTime": actualDate.strftime(TKS_DATE_TIME_FORMAT)} | rawBond 3886 3887 # Widen raw data with bond payment calendar (iData["rawCalendar"]): 3888 iData = iData | {"rawCalendar": self.RequestBondCoupons(iJSON=iData)} 3889 3890 # Replace some values with human-readable: 3891 iData["nominalCurrency"] = iData["nominal"]["currency"] 3892 iData["nominal"] = NanoToFloat(iData["nominal"]["units"], iData["nominal"]["nano"]) 3893 iData["placementPrice"] = NanoToFloat(iData["placementPrice"]["units"], iData["placementPrice"]["nano"]) 3894 iData["aciCurrency"] = iData["aciValue"]["currency"] 3895 iData["aciValue"] = NanoToFloat(iData["aciValue"]["units"], iData["aciValue"]["nano"]) 3896 iData["issueSize"] = int(iData["issueSize"]) 3897 iData["issueSizePlan"] = int(iData["issueSizePlan"]) 3898 iData["tradingStatus"] = TKS_TRADING_STATUSES[iData["tradingStatus"]] 3899 iData["step"] = iData["step"] if "step" in iData.keys() else 0 3900 iData["realExchange"] = TKS_REAL_EXCHANGES[iData["realExchange"]] 3901 iData["klong"] = NanoToFloat(iData["klong"]["units"], iData["klong"]["nano"]) if "klong" in iData.keys() else 0 3902 iData["kshort"] = NanoToFloat(iData["kshort"]["units"], iData["kshort"]["nano"]) if "kshort" in iData.keys() else 0 3903 iData["dlong"] = NanoToFloat(iData["dlong"]["units"], iData["dlong"]["nano"]) if "dlong" in iData.keys() else 0 3904 iData["dshort"] = NanoToFloat(iData["dshort"]["units"], iData["dshort"]["nano"]) if "dshort" in iData.keys() else 0 3905 iData["dlongMin"] = NanoToFloat(iData["dlongMin"]["units"], iData["dlongMin"]["nano"]) if "dlongMin" in iData.keys() else 0 3906 iData["dshortMin"] = NanoToFloat(iData["dshortMin"]["units"], iData["dshortMin"]["nano"]) if "dshortMin" in iData.keys() else 0 3907 3908 # Widen raw data with price fields from `currentPrice` values (all prices are actual at `actualDateTime` date): 3909 iData["limitUpPercent"] = iData["currentPrice"]["limitUp"] # max price on current day in percents of nominal 3910 iData["limitDownPercent"] = iData["currentPrice"]["limitDown"] # min price on current day in percents of nominal 3911 iData["lastPricePercent"] = iData["currentPrice"]["lastPrice"] # last price on market in percents of nominal 3912 iData["closePricePercent"] = iData["currentPrice"]["closePrice"] # previous day close in percents of nominal 3913 iData["changes"] = iData["currentPrice"]["changes"] # this is percent of changes between `currentPrice` and `lastPrice` 3914 iData["limitUp"] = iData["limitUpPercent"] * iData["nominal"] / 100 # max price on current day is `limitUpPercent` * `nominal` 3915 iData["limitDown"] = iData["limitDownPercent"] * iData["nominal"] / 100 # min price on current day is `limitDownPercent` * `nominal` 3916 iData["lastPrice"] = iData["lastPricePercent"] * iData["nominal"] / 100 # last price on market is `lastPricePercent` * `nominal` 3917 iData["closePrice"] = iData["closePricePercent"] * iData["nominal"] / 100 # previous day close is `closePricePercent` * `nominal` 3918 iData["changesDelta"] = iData["lastPrice"] - iData["closePrice"] # this is delta between last deal price and last close 3919 3920 # Widen raw data with calendar data from `rawCalendar` values: 3921 calendarData = [] 3922 for item in iData["rawCalendar"]["events"]: 3923 calendarData.append({ 3924 "couponDate": item["couponDate"], 3925 "couponNumber": int(item["couponNumber"]), 3926 "fixDate": item["fixDate"] if "fixDate" in item.keys() else "", 3927 "payCurrency": item["payOneBond"]["currency"], 3928 "payOneBond": NanoToFloat(item["payOneBond"]["units"], item["payOneBond"]["nano"]), 3929 "couponType": TKS_COUPON_TYPES[item["couponType"]], 3930 "couponStartDate": item["couponStartDate"], 3931 "couponEndDate": item["couponEndDate"], 3932 "couponPeriod": item["couponPeriod"], 3933 }) 3934 3935 # if maturity date is unknown then uses the latest date in bond payment calendar for it: 3936 if "maturityDate" not in iData.keys(): 3937 iData["maturityDate"] = datetime.strptime(calendarData[0]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_DATE_TIME_FORMAT) if calendarData else "" 3938 3939 # Widen raw data with Coupon Rate. 3940 # This is sum of all coupon payments divided on nominal price and expire days sum and then multiple on 365 days and 100%: 3941 iData["sumCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData]) 3942 iData["periodDays"] = sum([coupon["couponPeriod"] for coupon in calendarData]) 3943 iData["couponsYield"] = 100 * 365 * (iData["sumCoupons"] / iData["nominal"]) / iData["periodDays"] if iData["nominal"] != 0 and iData["periodDays"] != 0 else 0. 3944 3945 # Widen raw data with Yield to Maturity (YTM) on current date. 3946 # This is sum of all stayed coupons to maturity minus ACI and divided on current bond price and then multiple on stayed days and 100%: 3947 maturityDate = datetime.strptime(iData["maturityDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) if iData["maturityDate"] else None 3948 iData["daysToMaturity"] = (maturityDate - actualDate).days if iData["maturityDate"] else None 3949 iData["sumLastCoupons"] = sum([coupon["payOneBond"] for coupon in calendarData if datetime.strptime(coupon["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) > actualDate]) 3950 iData["lastPayments"] = iData["sumLastCoupons"] - iData["aciValue"] # sum of all last coupons minus current ACI value 3951 iData["currentYield"] = 100 * 365 * (iData["lastPayments"] / iData["lastPrice"]) / iData["daysToMaturity"] if iData["lastPrice"] != 0 and iData["daysToMaturity"] != 0 else 0. 3952 3953 iData["calendar"] = calendarData # adds calendar at the end 3954 3955 # Remove not used data: 3956 iData.pop("uid") 3957 iData.pop("positionUid") 3958 iData.pop("currentPrice") 3959 iData.pop("rawCalendar") 3960 3961 colNames = list(iData.keys()) 3962 if bonds is None: 3963 bonds = pd.DataFrame(data=pd.DataFrame.from_records(data=[iData], columns=colNames)) 3964 3965 else: 3966 bonds = pd.concat([bonds, pd.DataFrame.from_records(data=[iData], columns=colNames)], axis=0, ignore_index=True) 3967 3968 else: 3969 uLogger.warning("Instrument with ticker [{}] and FIGI [{}] is not a bond!".format(instrument["ticker"], instrument["figi"])) 3970 3971 processed = round(100 * (i + 1) / iCount, 1) 3972 if tooLong and processed % 5 == 0: 3973 uLogger.info("{}% processed [{} / {}]...".format(round(processed), i + 1, iCount)) 3974 3975 else: 3976 uLogger.debug("{}% bonds processed [{} / {}]...".format(processed, i + 1, iCount)) 3977 3978 bonds.index = bonds["ticker"].tolist() # replace indexes with ticker names 3979 3980 # Saving bonds from Pandas DataFrame to XLSX sheet: 3981 if xlsx and self.bondsXLSXFile: 3982 with pd.ExcelWriter( 3983 path=self.bondsXLSXFile, 3984 date_format=TKS_DATE_FORMAT, 3985 datetime_format=TKS_DATE_TIME_FORMAT, 3986 mode="w", 3987 ) as writer: 3988 bonds.to_excel( 3989 writer, 3990 sheet_name="Extended bonds data", 3991 index=True, 3992 encoding="UTF-8", 3993 freeze_panes=(1, 1), 3994 ) # saving as XLSX-file with freeze first row and column as headers 3995 3996 uLogger.info("XLSX-file with extended bonds data for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(self.bondsXLSXFile))) 3997 3998 return bonds
Requests jsons with raw bonds data for every ticker or FIGI in instruments list and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowInstrumentInfo(), CreateBondsCalendar(), ShowBondsCalendar(), RequestBondCoupons().
Parameters
- instruments: list of strings with tickers or FIGIs.
- xlsx: if True then also exports Pandas DataFrame to xlsx-file
bondsXLSXFile, defaultext-bonds.xlsx, for further used by data scientists or stock analytics.
Returns
wider Pandas DataFrame with more full and calculated data about bonds, than raw response from broker. In XLSX-file and Pandas DataFrame fields mean: - main info about bond: https://tinkoff.github.io/investAPI/instruments/#bond - info about coupon: https://tinkoff.github.io/investAPI/instruments/#coupon
4000 def CreateBondsCalendar(self, extBonds: pd.DataFrame, xlsx: bool = False) -> pd.DataFrame: 4001 """ 4002 Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, `calendar.xlsx` by default. 4003 4004 WARNING! This is too long operation if a lot of bonds requested from broker server. 4005 4006 See also: `ShowBondsCalendar()`, `ExtendBondsData()`. 4007 4008 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4009 extended information about bonds: main info, current prices, bond payment calendar, 4010 coupon yields, current yields and some statistics etc. 4011 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4012 :param xlsx: if True then also exports Pandas DataFrame to file `calendarFile` + `".xlsx"`, `calendar.xlsx` by default, 4013 for further used by data scientists or stock analytics. 4014 :return: Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon 4015 """ 4016 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4017 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4018 4019 uLogger.debug("Generating bond payments calendar data. Wait, please...") 4020 4021 colNames = ["Paid", "Payment date", "FIGI", "Ticker", "Name", "No.", "Value", "Currency", "Coupon type", "Period", "End registry date", "Coupon start date", "Coupon end date"] 4022 colID = ["paid", "couponDate", "figi", "ticker", "name", "couponNumber", "payOneBond", "payCurrency", "couponType", "couponPeriod", "fixDate", "couponStartDate", "couponEndDate"] 4023 calendar = None 4024 for bond in extBonds.iterrows(): 4025 for item in bond[1]["calendar"]: 4026 cData = { 4027 "paid": datetime.now(tzutc()) > datetime.strptime(item["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()), 4028 "couponDate": item["couponDate"], 4029 "figi": bond[1]["figi"], 4030 "ticker": bond[1]["ticker"], 4031 "name": bond[1]["name"], 4032 "couponNumber": item["couponNumber"], 4033 "payOneBond": item["payOneBond"], 4034 "payCurrency": item["payCurrency"], 4035 "couponType": item["couponType"], 4036 "couponPeriod": item["couponPeriod"], 4037 "fixDate": item["fixDate"], 4038 "couponStartDate": item["couponStartDate"], 4039 "couponEndDate": item["couponEndDate"], 4040 } 4041 4042 if calendar is None: 4043 calendar = pd.DataFrame(data=pd.DataFrame.from_records(data=[cData], columns=colID)) 4044 4045 else: 4046 calendar = pd.concat([calendar, pd.DataFrame.from_records(data=[cData], columns=colID)], axis=0, ignore_index=True) 4047 4048 calendar = calendar.sort_values(by=["couponDate"], axis=0, ascending=True) # sort all payments for all bonds by payment date 4049 4050 # Saving calendar from Pandas DataFrame to XLSX sheet: 4051 if xlsx: 4052 xlsxCalendarFile = self.calendarFile.replace(".md", ".xlsx") if self.calendarFile.endswith(".md") else self.calendarFile + ".xlsx" 4053 4054 with pd.ExcelWriter( 4055 path=xlsxCalendarFile, 4056 date_format=TKS_DATE_FORMAT, 4057 datetime_format=TKS_DATE_TIME_FORMAT, 4058 mode="w", 4059 ) as writer: 4060 humanReadable = calendar.copy(deep=True) 4061 humanReadable["couponDate"] = humanReadable["couponDate"].apply(lambda x: x.split("T")[0]) 4062 humanReadable["fixDate"] = humanReadable["fixDate"].apply(lambda x: x.split("T")[0]) 4063 humanReadable["couponStartDate"] = humanReadable["couponStartDate"].apply(lambda x: x.split("T")[0]) 4064 humanReadable["couponEndDate"] = humanReadable["couponEndDate"].apply(lambda x: x.split("T")[0]) 4065 humanReadable.columns = colNames # human-readable column names 4066 4067 humanReadable.to_excel( 4068 writer, 4069 sheet_name="Bond payments calendar", 4070 index=False, 4071 encoding="UTF-8", 4072 freeze_panes=(1, 2), 4073 ) # saving as XLSX-file with freeze first row and column as headers 4074 4075 del humanReadable # release df in memory 4076 4077 uLogger.info("XLSX-file with bond payments calendar for further used by data scientists or stock analytics: [{}]".format(os.path.abspath(xlsxCalendarFile))) 4078 4079 return calendar
Creates bond payments calendar as Pandas DataFrame, and also save it to the XLSX-file, calendar.xlsx by default.
WARNING! This is too long operation if a lot of bonds requested from broker server.
See also: ShowBondsCalendar(), ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - xlsx: if True then also exports Pandas DataFrame to file
calendarFile+".xlsx",calendar.xlsxby default, for further used by data scientists or stock analytics.
Returns
Pandas DataFrame with only bond payments calendar data. Fields mean: https://tinkoff.github.io/investAPI/instruments/#coupon
4081 def ShowBondsCalendar(self, extBonds: pd.DataFrame, show: bool = True) -> str: 4082 """ 4083 Show bond payments calendar as a table. One row in input `bonds` dataframe contains one bond. 4084 Also, creates Markdown file with calendar data, `calendar.md` by default. 4085 4086 See also: `ShowInstrumentInfo()`, `RequestBondCoupons()`, `CreateBondsCalendar()` and `ExtendBondsData()`. 4087 4088 :param extBonds: Pandas DataFrame object returns by `ExtendBondsData()` method and contains 4089 extended information about bonds: main info, current prices, bond payment calendar, 4090 coupon yields, current yields and some statistics etc. 4091 If this parameter is `None` then used `figi` or `ticker` as bond name and then calculate `ExtendBondsData()`. 4092 :param show: if `True` then also printing bonds payment calendar to the console, 4093 otherwise save to file `calendarFile` only. `False` by default. 4094 :return: multilines text in Markdown format with bonds payment calendar as a table. 4095 """ 4096 if extBonds is None or not isinstance(extBonds, pd.DataFrame) or extBonds.empty: 4097 extBonds = self.ExtendBondsData(instruments=[self.figi, self.ticker], xlsx=False) 4098 4099 infoText = "# Bond payments calendar\n\n" 4100 4101 calendar = self.CreateBondsCalendar(extBonds, xlsx=True) # generate Pandas DataFrame with full calendar data 4102 4103 if not calendar.empty: 4104 splitLine = "| | | | | | | | | |\n" 4105 4106 info = [ 4107 "| Paid | Payment date | FIGI | Ticker | No. | Value | Type | Period | End registry date |\n", 4108 "|-------|-----------------|--------------|--------------|-----|---------------|-----------|--------|-------------------|\n", 4109 ] 4110 4111 newMonth = False 4112 notOneBond = calendar["figi"].nunique() > 1 4113 for i, bond in enumerate(calendar.iterrows()): 4114 if newMonth and notOneBond: 4115 info.append(splitLine) 4116 4117 info.append( 4118 "| {:<5} | {:<15} | {:<12} | {:<12} | {:<3} | {:<13} | {:<9} | {:<6} | {:<17} |\n".format( 4119 " √" if bond[1]["paid"] else " —", 4120 bond[1]["couponDate"].split("T")[0], 4121 bond[1]["figi"], 4122 bond[1]["ticker"], 4123 bond[1]["couponNumber"], 4124 "{} {}".format( 4125 "{}".format(round(bond[1]["payOneBond"], 6)).rstrip("0").rstrip("."), 4126 bond[1]["payCurrency"], 4127 ), 4128 bond[1]["couponType"], 4129 bond[1]["couponPeriod"], 4130 bond[1]["fixDate"].split("T")[0], 4131 ) 4132 ) 4133 4134 if i < len(calendar.values) - 1: 4135 curDate = datetime.strptime(bond[1]["couponDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4136 nextDate = datetime.strptime(calendar["couponDate"].values[i + 1], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()) 4137 newMonth = False if curDate.month == nextDate.month else True 4138 4139 else: 4140 newMonth = False 4141 4142 infoText += "".join(info) 4143 4144 if show: 4145 uLogger.info("{}".format(infoText)) 4146 4147 if self.calendarFile is not None: 4148 with open(self.calendarFile, "w", encoding="UTF-8") as fH: 4149 fH.write(infoText) 4150 4151 uLogger.info("Bond payment calendar was saved to file: [{}]".format(os.path.abspath(self.calendarFile))) 4152 4153 else: 4154 infoText += "No data\n" 4155 4156 return infoText
Show bond payments calendar as a table. One row in input bonds dataframe contains one bond.
Also, creates Markdown file with calendar data, calendar.md by default.
See also: ShowInstrumentInfo(), RequestBondCoupons(), CreateBondsCalendar() and ExtendBondsData().
Parameters
- extBonds: Pandas DataFrame object returns by
ExtendBondsData()method and contains extended information about bonds: main info, current prices, bond payment calendar, coupon yields, current yields and some statistics etc. If this parameter isNonethen usedfigiortickeras bond name and then calculateExtendBondsData(). - show: if
Truethen also printing bonds payment calendar to the console, otherwise save to filecalendarFileonly.Falseby default.
Returns
multilines text in Markdown format with bonds payment calendar as a table.
4158 def OverviewAccounts(self, show: bool = False) -> dict: 4159 """ 4160 Method for parsing and show simple table with all available user accounts. 4161 4162 See also: `RequestAccounts()` and `OverviewUserInfo()` methods. 4163 4164 :param show: if `False` then only dictionary with accounts data returns, if `True` then also print it to log. 4165 :return: dict with parsed accounts data received from `RequestAccounts()` method. Example of dict: 4166 `view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, 4167 "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", 4168 "status": "Opened and active account", "opened": "2018-05-23 00:00:00", 4169 "closed": "—", "access": "Full access" }, ...}}` 4170 """ 4171 rawAccounts = self.RequestAccounts() # Raw responses with accounts 4172 4173 # This is an array of dict with user accounts, its `accountId`s and some parsed data: 4174 accounts = { 4175 item["id"]: { 4176 "type": TKS_ACCOUNT_TYPES[item["type"]], 4177 "name": item["name"], 4178 "status": TKS_ACCOUNT_STATUSES[item["status"]], 4179 "opened": datetime.strptime(item["openedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4180 "closed": datetime.strptime(item["closedDate"], TKS_DATE_TIME_FORMAT).replace(tzinfo=tzutc()).astimezone(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT) if item["closedDate"] != "1970-01-01T00:00:00Z" else "—", 4181 "access": TKS_ACCESS_LEVELS[item["accessLevel"]], 4182 } for item in rawAccounts["accounts"] 4183 } 4184 4185 # Raw and parsed data with some fields replaced in "stat" section: 4186 view = { 4187 "rawAccounts": rawAccounts, 4188 "stat": accounts, 4189 } 4190 4191 # --- Prepare simple text table with only accounts data in human-readable format: 4192 if show: 4193 info = [ 4194 "# User accounts\n\n", 4195 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4196 "| Account ID | Type | Status | Name |\n", 4197 "|--------------|---------------------------|---------------------------|--------------------------------|\n", 4198 ] 4199 4200 for account in view["stat"].keys(): 4201 info.extend([ 4202 "| {:<12} | {:<25} | {:<25} | {:<30} |\n".format( 4203 account, 4204 view["stat"][account]["type"], 4205 view["stat"][account]["status"], 4206 view["stat"][account]["name"], 4207 ) 4208 ]) 4209 4210 infoText = "".join(info) 4211 4212 uLogger.info(infoText) 4213 4214 if self.userAccountsFile: 4215 with open(self.userAccountsFile, "w", encoding="UTF-8") as fH: 4216 fH.write(infoText) 4217 4218 uLogger.info("User accounts were saved to file: [{}]".format(os.path.abspath(self.userAccountsFile))) 4219 4220 return view
Method for parsing and show simple table with all available user accounts.
See also: RequestAccounts() and OverviewUserInfo() methods.
Parameters
- show: if
Falsethen only dictionary with accounts data returns, ifTruethen also print it to log.
Returns
dict with parsed accounts data received from
RequestAccounts()method. Example of dict:view = {"rawAccounts": {rawAccounts from RequestAccounts() method...}, "stat": {"accountId string": {"type": "Tinkoff brokerage account", "name": "Test - 1", "status": "Opened and active account", "opened": "2018-05-23 00:00:00", "closed": "—", "access": "Full access" }, ...}}
4222 def OverviewUserInfo(self, show: bool = False) -> dict: 4223 """ 4224 Method for parsing and show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). 4225 4226 See also: `OverviewAccounts()`, `RequestAccounts()`, `RequestUserInfo()`, `RequestMarginStatus()` and `RequestTariffLimits()` methods. 4227 4228 :param show: if `False` then only dictionary returns, if `True` then also print user's data to log. 4229 :return: dict with raw parsed data from server and some calculated statistics about it. 4230 """ 4231 rawUserInfo = self.RequestUserInfo() # Raw response with common user info 4232 overviewAccount = self.OverviewAccounts(show=False) # Raw and parsed accounts data 4233 rawAccounts = overviewAccount["rawAccounts"] # Raw response with user accounts data 4234 accounts = overviewAccount["stat"] # Dict with only statistics about user accounts 4235 rawMargins = {account: self.RequestMarginStatus(accountId=account) for account in accounts.keys()} # Raw response with margin calculation for every account ID 4236 rawTariffLimits = self.RequestTariffLimits() # Raw response with limits of current tariff 4237 4238 # This is dict with parsed common user data: 4239 userInfo = { 4240 "premium": "Yes" if rawUserInfo["premStatus"] else "No", 4241 "qualified": "Yes" if rawUserInfo["qualStatus"] else "No", 4242 "allowed": [TKS_QUALIFIED_TYPES[item] for item in rawUserInfo["qualifiedForWorkWith"]], 4243 "tariff": rawUserInfo["tariff"], 4244 } 4245 4246 # This is an array of dict with parsed margin statuses for every account IDs: 4247 margins = {} 4248 for accountId in accounts.keys(): 4249 if rawMargins[accountId]: 4250 margins[accountId] = { 4251 "currency": rawMargins[accountId]["liquidPortfolio"]["currency"], 4252 "liquid": NanoToFloat(rawMargins[accountId]["liquidPortfolio"]["units"], rawMargins[accountId]["liquidPortfolio"]["nano"]), 4253 "start": NanoToFloat(rawMargins[accountId]["startingMargin"]["units"], rawMargins[accountId]["startingMargin"]["nano"]), 4254 "min": NanoToFloat(rawMargins[accountId]["minimalMargin"]["units"], rawMargins[accountId]["minimalMargin"]["nano"]), 4255 "level": NanoToFloat(rawMargins[accountId]["fundsSufficiencyLevel"]["units"], rawMargins[accountId]["fundsSufficiencyLevel"]["nano"]), 4256 "missing": NanoToFloat(rawMargins[accountId]["amountOfMissingFunds"]["units"], rawMargins[accountId]["amountOfMissingFunds"]["nano"]), 4257 } 4258 4259 else: 4260 margins[accountId] = {} # Server response: margin status is disabled for current accountId 4261 4262 unary = {} # unary-connection limits 4263 for item in rawTariffLimits["unaryLimits"]: 4264 if item["limitPerMinute"] in unary.keys(): 4265 unary[item["limitPerMinute"]].extend(item["methods"]) 4266 4267 else: 4268 unary[item["limitPerMinute"]] = item["methods"] 4269 4270 stream = {} # stream-connection limits 4271 for item in rawTariffLimits["streamLimits"]: 4272 if item["limit"] in stream.keys(): 4273 stream[item["limit"]].extend(item["streams"]) 4274 4275 else: 4276 stream[item["limit"]] = item["streams"] 4277 4278 # This is dict with parsed limits of current tariff (connections, API methods etc.): 4279 limits = { 4280 "unary": unary, 4281 "stream": stream, 4282 } 4283 4284 # Raw and parsed data as an output result: 4285 view = { 4286 "rawUserInfo": rawUserInfo, 4287 "rawAccounts": rawAccounts, 4288 "rawMargins": rawMargins, 4289 "rawTariffLimits": rawTariffLimits, 4290 "stat": { 4291 "userInfo": userInfo, 4292 "accounts": accounts, 4293 "margins": margins, 4294 "limits": limits, 4295 }, 4296 } 4297 4298 # --- Prepare text table with user information in human-readable format: 4299 if show: 4300 info = [ 4301 "# Full user information\n\n", 4302 "* **Actual date:** [{} UTC]\n\n".format(datetime.now(tzutc()).strftime(TKS_PRINT_DATE_TIME_FORMAT)), 4303 "## Common information\n\n", 4304 "* **Qualified user:** {}\n".format(view["stat"]["userInfo"]["qualified"]), 4305 "* **Tariff name:** {}\n".format(view["stat"]["userInfo"]["tariff"]), 4306 "* **Premium user:** {}\n".format(view["stat"]["userInfo"]["premium"]), 4307 "* **Allowed to work with instruments:**\n{}\n".format("".join([" - {}\n".format(item) for item in view["stat"]["userInfo"]["allowed"]])), 4308 "\n## User accounts\n\n", 4309 ] 4310 4311 for account in view["stat"]["accounts"].keys(): 4312 info.extend([ 4313 "### ID: [{}]\n\n".format(account), 4314 "| Parameters | Values |\n", 4315 "|----------------------|--------------------------------------------------------------|\n", 4316 "| Account type: | {:<60} |\n".format(view["stat"]["accounts"][account]["type"]), 4317 "| Account name: | {:<60} |\n".format(view["stat"]["accounts"][account]["name"]), 4318 "| Account status: | {:<60} |\n".format(view["stat"]["accounts"][account]["status"]), 4319 "| Access level: | {:<60} |\n".format(view["stat"]["accounts"][account]["access"]), 4320 "| Date opened: | {:<60} |\n".format(view["stat"]["accounts"][account]["opened"]), 4321 "| Date closed: | {:<60} |\n".format(view["stat"]["accounts"][account]["closed"]), 4322 ]) 4323 4324 if margins[account]: 4325 info.extend([ 4326 "| Margin status: | Enabled |\n", 4327 "| - Liquid portfolio: | {:<60} |\n".format("{} {}".format(margins[account]["liquid"], margins[account]["currency"])), 4328 "| - Margin starting: | {:<60} |\n".format("{} {}".format(margins[account]["start"], margins[account]["currency"])), 4329 "| - Margin minimum: | {:<60} |\n".format("{} {}".format(margins[account]["min"], margins[account]["currency"])), 4330 "| - Sufficiency level: | {:<60} |\n".format("{:.2f} ({:.2f}%)".format(margins[account]["level"], margins[account]["level"] * 100)), 4331 "| - Missing funds: | {:<60} |\n\n".format("{} {}".format(margins[account]["missing"], margins[account]["currency"])), 4332 ]) 4333 4334 else: 4335 info.append("| Margin status: | Disabled |\n\n") 4336 4337 info.extend([ 4338 "\n## Current user tariff limits\n", 4339 "\nSee also:\n", 4340 "* Tinkoff limit policy: https://tinkoff.github.io/investAPI/limits/\n", 4341 "* Tinkoff Invest API: https://tinkoff.github.io/investAPI/\n", 4342 " - More about REST API requests: https://tinkoff.github.io/investAPI/swagger-ui/\n", 4343 " - More about gRPC requests for stream connections: https://tinkoff.github.io/investAPI/grpc/\n", 4344 "\n### Unary limits\n", 4345 ]) 4346 4347 if unary: 4348 for key, values in sorted(unary.items()): 4349 info.append("\n* Max requests per minute: {}\n".format(key)) 4350 4351 for value in values: 4352 info.append(" - {}\n".format(value)) 4353 4354 else: 4355 info.append("\nNot available\n") 4356 4357 info.append("\n### Stream limits\n") 4358 4359 if stream: 4360 for key, values in sorted(stream.items()): 4361 info.append("\n* Max stream connections: {}\n".format(key)) 4362 4363 for value in values: 4364 info.append(" - {}\n".format(value)) 4365 4366 else: 4367 info.append("\nNot available\n") 4368 4369 infoText = "".join(info) 4370 4371 uLogger.info(infoText) 4372 4373 if self.userInfoFile: 4374 with open(self.userInfoFile, "w", encoding="UTF-8") as fH: 4375 fH.write(infoText) 4376 4377 uLogger.info("User data was saved to file: [{}]".format(os.path.abspath(self.userInfoFile))) 4378 4379 return view
Method for parsing and show all available user's data (accountIds, common user information, margin status and tariff connections limit).
See also: OverviewAccounts(), RequestAccounts(), RequestUserInfo(), RequestMarginStatus() and RequestTariffLimits() methods.
Parameters
- show: if
Falsethen only dictionary returns, ifTruethen also print user's data to log.
Returns
dict with raw parsed data from server and some calculated statistics about it.
4382class Args: 4383 """ 4384 If `Main()` function is imported as module, then this class used to convert arguments from **kwargs as object. 4385 """ 4386 def __init__(self, **kwargs): 4387 self.__dict__.update(kwargs) 4388 4389 def __getattr__(self, item): 4390 return None
If Main() function is imported as module, then this class used to convert arguments from **kwargs as object.
4393def ParseArgs(): 4394 """This function get and parse command line keys.""" 4395 parser = ArgumentParser() # command-line string parser 4396 4397 parser.description = "TKSBrokerAPI is a trading platform for automation on Python to simplify the implementation of trading scenarios and work with Tinkoff Invest API server via the REST protocol. See examples: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md" 4398 parser.usage = "\n/as module/ python TKSBrokerAPI.py [some options] [one command]\n/as CLI tool/ tksbrokerapi [some options] [one command]" 4399 4400 # --- options: 4401 4402 parser.add_argument("--no-cache", action="store_true", default=False, help="Option: not use local cache `dump.json`, but update raw instruments data when starting the platform. `False` by default.") 4403 parser.add_argument("--token", type=str, help="Option: Tinkoff service's api key. If not set then used environment variable `TKS_API_TOKEN`. See how to use: https://tinkoff.github.io/investAPI/token/") 4404 parser.add_argument("--account-id", type=str, default=None, help="Option: string with an user numeric account ID in Tinkoff Broker. It can be found in any broker's reports (see the contract number). Also, this variable can be set from environment variable `TKS_ACCOUNT_ID`.") 4405 4406 parser.add_argument("--ticker", "-t", type=str, help="Option: instrument's ticker, e.g. `IBM`, `YNDX`, `GOOGL` etc. Use alias for `USD000UTSTOM` simple as `USD`, `EUR_RUB__TOM` as `EUR`.") 4407 parser.add_argument("--figi", "-f", type=str, help="Option: instrument's FIGI, e.g. `BBG006L8G4H1` (for `YNDX`).") 4408 4409 parser.add_argument("--depth", type=int, default=1, help="Option: Depth of Market (DOM) can be >=1, 1 by default.") 4410 parser.add_argument("--no-cancelled", "--no-canceled", action="store_true", default=False, help="Option: remove information about cancelled operations from the deals report by the `--deals` key. `False` by default.") 4411 4412 parser.add_argument("--output", type=str, default=None, help="Option: replace default paths to output files for some commands. If `None` then used default files.") 4413 4414 parser.add_argument("--interval", type=str, default="hour", help="Option: available values are `1min`, `5min`, `15min`, `hour` and `day`. Used only with `--history` key. This is time period of one candle. Default: `hour` for every history candles.") 4415 parser.add_argument("--only-missing", action="store_true", default=False, help="Option: if history file define by `--output` key then add only last missing candles, do not request all history length. `False` by default.") 4416 parser.add_argument("--csv-sep", type=str, default=",", help="Option: separator if csv-file is used, `,` by default.") 4417 4418 parser.add_argument("--debug-level", "--verbosity", "-v", type=int, default=20, help="Option: showing STDOUT messages of minimal debug level, e.g. 10 = DEBUG, 20 = INFO, 30 = WARNING, 40 = ERROR, 50 = CRITICAL. INFO (20) by default.") 4419 4420 # --- commands: 4421 4422 parser.add_argument("--version", "--ver", action="store_true", help="Action: shows current semantic version, looks like `major.minor.buildnumber`. If TKSBrokerAPI not installed via pip, then used local build number `.dev0`.") 4423 4424 parser.add_argument("--list", "-l", action="store_true", help="Action: get and print all available instruments and some information from broker server. Also, you can define `--output` key to save list of instruments to file, default: `instruments.md`.") 4425 parser.add_argument("--list-xlsx", "-x", action="store_true", help="Action: get all available instruments from server for current account and save raw data into xlsx-file for further used by data scientists or stock analytics, default: `dump.xlsx`.") 4426 parser.add_argument("--bonds-xlsx", "-b", type=str, nargs="*", help="Action: get all available bonds if only key present or list of bonds with FIGIs or tickers and transform it to the wider Pandas DataFrame with more information about bonds: main info, current prices, bonds payment calendar, coupon yields, current yields and some statistics etc. And then export data to XLSX-file, default: `ext-bonds.xlsx` or you can change it with `--output` key. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4427 parser.add_argument("--search", "-s", type=str, nargs=1, help="Action: search for an instruments by part of the name, ticker or FIGI. Also, you can define `--output` key to save results to file, default: `search-results.md`.") 4428 parser.add_argument("--info", "-i", action="store_true", help="Action: get information from broker server about instrument by it's ticker or FIGI. `--ticker` key or `--figi` key must be defined!") 4429 parser.add_argument("--calendar", "-c", type=str, nargs="*", help="Action: show bonds payment calendar as a table. Calendar build for one or more tickers or FIGIs, or for all bonds if only key present. If the `--output` key present then calendar saves to file, default: `calendar.md`. Also, created XLSX-file with bond payments calendar for further used by data scientists or stock analytics, `calendar.xlsx` by default. WARNING! This is too long operation if a lot of bonds requested from broker server.") 4430 parser.add_argument("--price", action="store_true", help="Action: show actual price list for current instrument. Also, you can use `--depth` key. `--ticker` key or `--figi` key must be defined!") 4431 parser.add_argument("--prices", "-p", type=str, nargs="+", help="Action: get and print current prices for list of given instruments (by it's tickers or by FIGIs). WARNING! This is too long operation if you request a lot of instruments! Also, you can define `--output` key to save list of prices to file, default: `prices.md`.") 4432 4433 parser.add_argument("--overview", "-o", action="store_true", help="Action: shows all open positions, orders and some statistics. Also, you can define `--output` key to save this information to file, default: `overview.md`.") 4434 parser.add_argument("--overview-digest", action="store_true", help="Action: shows a short digest of the portfolio status. Also, you can define `--output` key to save this information to file, default: `overview-digest.md`.") 4435 parser.add_argument("--overview-positions", action="store_true", help="Action: shows only open positions. Also, you can define `--output` key to save this information to file, default: `overview-positions.md`.") 4436 parser.add_argument("--overview-orders", action="store_true", help="Action: shows only sections of open limits and stop orders. Also, you can define `--output` key to save orders to file, default: `overview-orders.md`.") 4437 parser.add_argument("--overview-analytics", action="store_true", help="Action: shows only the analytics section and the distribution of the portfolio by various categories. Also, you can define `--output` key to save this information to file, default: `overview-analytics.md`.") 4438 4439 parser.add_argument("--deals", "-d", type=str, nargs="*", help="Action: show all deals between two given dates. Start day may be an integer number: -1, -2, -3 days ago. Also, you can use keywords: `today`, `yesterday` (-1), `week` (-7), `month` (-30) and `year` (-365). Dates format must be: `%%Y-%%m-%%d`, e.g. 2020-02-03. With `--no-cancelled` key information about cancelled operations will be removed from the deals report. Also, you can define `--output` key to save all deals to file, default: `deals.md`.") 4440 parser.add_argument("--history", type=str, nargs="*", help="Action: get last history candles of the current instrument defined by `--ticker` or `--figi` (FIGI id) keys. History returned between two given dates: `start` and `end`. Minimum requested date in the past is `1970-01-01`. This action may be used together with the `--render-chart` key. Also, you can define `--output` key to save history candlesticks to file.") 4441 parser.add_argument("--load-history", type=str, help="Action: try to load history candles from given csv-file as a Pandas Dataframe and print it in to the console. This action may be used together with the `--render-chart` key.") 4442 parser.add_argument("--render-chart", type=str, help="Action: render candlesticks chart. This key may only used with `--history` or `--load-history` together. Action has 1 parameter with two possible string values: `interact` (`i`) or `non-interact` (`ni`).") 4443 4444 parser.add_argument("--trade", nargs="*", help="Action: universal action to open market position for defined ticker or FIGI. You must specify 1-5 parameters: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. See examples in readme.") 4445 parser.add_argument("--buy", nargs="*", help="Action: immediately open BUY market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4446 parser.add_argument("--sell", nargs="*", help="Action: immediately open SELL market position at the current price for defined ticker or FIGI. You must specify 0-4 parameters: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`].") 4447 4448 parser.add_argument("--order", nargs="*", help="Action: universal action to open limit or stop-order in any directions. You must specify 4-7 parameters: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]]. See examples in readme.") 4449 parser.add_argument("--buy-limit", type=float, nargs=2, help="Action: open pending BUY limit-order (below current price). You must specify only 2 parameters: [lots] [target price] to open BUY limit-order. If you try to create `Buy` limit-order above current price then broker immediately open `Buy` market order, such as if you do simple `--buy` operation!") 4450 parser.add_argument("--sell-limit", type=float, nargs=2, help="Action: open pending SELL limit-order (above current price). You must specify only 2 parameters: [lots] [target price] to open SELL limit-order. If you try to create `Sell` limit-order below current price then broker immediately open `Sell` market order, such as if you do simple `--sell` operation!") 4451 parser.add_argument("--buy-stop", nargs="*", help="Action: open BUY stop-order. You must specify at least 2 parameters: [lots] [target price] to open BUY stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4452 parser.add_argument("--sell-stop", nargs="*", help="Action: open SELL stop-order. You must specify at least 2 parameters: [lots] [target price] to open SELL stop-order. In additional you can specify 3 parameters for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%%Y-%%m-%%d %%H:%%M:%%S`]. When current price will go up or down to target price value then broker opens a limit order. Stop loss order always executed by market price.") 4453 # parser.add_argument("--buy-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending BUY limit-orders (below current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4454 # parser.add_argument("--sell-limit-order-grid", type=str, nargs="*", help="Action: open grid of pending SELL limit-orders (above current price). Parameters format: l(ots)=[L_int,...] p(rices)=[P_float,...]. Counts of values in lots and prices lists must be equals!") 4455 4456 parser.add_argument("--close-order", "--cancel-order", type=str, nargs=1, help="Action: close only one order by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4457 parser.add_argument("--close-orders", "--cancel-orders", type=str, nargs="+", help="Action: close one or list of orders by it's `orderId` or `stopOrderId`. You can find out the meaning of these IDs using the key `--overview`.") 4458 parser.add_argument("--close-trade", "--cancel-trade", action="store_true", help="Action: close only one position for instrument defined by `--ticker` key, including for currencies tickers.") 4459 parser.add_argument("--close-trades", "--cancel-trades", type=str, nargs="+", help="Action: close positions for list of tickers, including for currencies tickers.") 4460 parser.add_argument("--close-all", "--cancel-all", type=str, nargs="*", help="Action: close all available (not blocked) opened trades and orders, excluding for currencies. Also you can select one or more keywords case insensitive to specify trades type: `orders`, `shares`, `bonds`, `etfs` and `futures`, but not `currencies`. Currency positions you must closes manually using `--buy`, `--sell`, `--close-trade` or `--close-trades` operations.") 4461 4462 parser.add_argument("--limits", "--withdrawal-limits", "-w", action="store_true", help="Action: show table of funds available for withdrawal for current `accountId`. You can change `accountId` with the key `--account-id`. Also, you can define `--output` key to save this information to file, default: `limits.md`.") 4463 parser.add_argument("--user-info", "-u", action="store_true", help="Action: show all available user's data (`accountId`s, common user information, margin status and tariff connections limit). Also, you can define `--output` key to save this information to file, default: `user-info.md`.") 4464 parser.add_argument("--account", "--accounts", "-a", action="store_true", help="Action: show simple table with all available user accounts. Also, you can define `--output` key to save this information to file, default: `accounts.md`.") 4465 4466 cmdArgs = parser.parse_args() 4467 return cmdArgs
This function get and parse command line keys.
4470def Main(**kwargs): 4471 """ 4472 Main function for work with TKSBrokerAPI in the console. 4473 4474 See examples: 4475 - in english: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README_EN.md 4476 - in russian: https://github.com/Tim55667757/TKSBrokerAPI/blob/master/README.md 4477 """ 4478 args = Args(**kwargs) if kwargs else ParseArgs() # get and parse command-line parameters or use **kwarg parameters 4479 4480 if args.debug_level: 4481 uLogger.level = 10 # always debug level by default 4482 uLogger.handlers[0].level = args.debug_level # level for STDOUT 4483 4484 exitCode = 0 4485 start = datetime.now(tzutc()) 4486 uLogger.debug(">>> TKSBrokerAPI module started at: [{}] UTC, it is [{}] local time".format( 4487 start.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4488 start.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4489 )) 4490 4491 # trying to calculate full current version: 4492 buildVersion = __version__ 4493 try: 4494 v = version("tksbrokerapi") 4495 buildVersion = v if v.startswith(buildVersion) else buildVersion + ".dev0" # set version as major.minor.dev0 if run as local build or local script 4496 4497 except Exception: 4498 buildVersion = __version__ + ".dev0" # if an errors occurred then also set version as major.minor.dev0 4499 4500 uLogger.debug("TKSBrokerAPI major.minor.build version used: [{}]".format(buildVersion)) 4501 uLogger.debug("Host CPU count: [{}]".format(CPU_COUNT)) 4502 4503 try: 4504 if args.version: 4505 print("TKSBrokerAPI {}".format(buildVersion)) 4506 uLogger.debug("User requested current TKSBrokerAPI major.minor.build version: [{}]".format(buildVersion)) 4507 4508 else: 4509 # Init class for trading with Tinkoff Broker: TODO: rename `server` to `trader` 4510 server = TinkoffBrokerServer( 4511 token=args.token, 4512 accountId=args.account_id, 4513 useCache=not args.no_cache, 4514 ) 4515 4516 # --- set some options: 4517 4518 if args.ticker: 4519 if args.ticker in server.aliasesKeys: 4520 server.ticker = server.aliases[args.ticker] # Replace some tickers with its aliases 4521 4522 else: 4523 server.ticker = args.ticker 4524 4525 if args.figi: 4526 server.figi = args.figi 4527 4528 if args.depth is not None: 4529 server.depth = args.depth 4530 4531 # --- do one of commands: 4532 4533 if args.list: 4534 if args.output is not None: 4535 server.instrumentsFile = args.output 4536 4537 server.ShowInstrumentsInfo(show=True) 4538 4539 elif args.list_xlsx: 4540 server.DumpInstrumentsAsXLSX(forceUpdate=False) 4541 4542 elif args.bonds_xlsx is not None: 4543 if args.output is not None: 4544 server.bondsXLSXFile = args.output 4545 4546 if len(args.bonds_xlsx) == 0: 4547 server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=True) # request bonds with all available tickers 4548 4549 else: 4550 server.ExtendBondsData(instruments=args.bonds_xlsx, xlsx=True) # request list of given bonds 4551 4552 elif args.search: 4553 if args.output is not None: 4554 server.searchResultsFile = args.output 4555 4556 server.SearchInstruments(pattern=args.search[0], show=True) 4557 4558 elif args.info: 4559 if not (args.ticker or args.figi): 4560 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4561 raise Exception("Ticker or FIGI required") 4562 4563 if args.output is not None: 4564 server.infoFile = args.output 4565 4566 if args.ticker: 4567 server.SearchByTicker(requestPrice=True, show=True, debug=False) # show info and current prices by ticker name 4568 4569 else: 4570 server.SearchByFIGI(requestPrice=True, show=True, debug=False) # show info and current prices by FIGI id 4571 4572 elif args.calendar is not None: 4573 if args.output is not None: 4574 server.calendarFile = args.output 4575 4576 if len(args.calendar) == 0: 4577 bondsData = server.ExtendBondsData(instruments=server.iList["Bonds"].keys(), xlsx=False) # request bonds with all available tickers 4578 4579 else: 4580 bondsData = server.ExtendBondsData(instruments=args.calendar, xlsx=False) # request list of given bonds 4581 4582 server.ShowBondsCalendar(extBonds=bondsData, show=True) # shows bonds payment calendar only 4583 4584 elif args.price: 4585 if not (args.ticker or args.figi): 4586 uLogger.error("`--ticker` key or `--figi` key is required for this operation!") 4587 raise Exception("Ticker or FIGI required") 4588 4589 server.GetCurrentPrices(show=True) 4590 4591 elif args.prices is not None: 4592 if args.output is not None: 4593 server.pricesFile = args.output 4594 4595 server.GetListOfPrices(instruments=args.prices, show=True) # WARNING: too long wait for a lot of instruments prices 4596 4597 elif args.overview: 4598 if args.output is not None: 4599 server.overviewFile = args.output 4600 4601 server.Overview(show=True, details="full") 4602 4603 elif args.overview_digest: 4604 if args.output is not None: 4605 server.overviewDigestFile = args.output 4606 4607 server.Overview(show=True, details="digest") 4608 4609 elif args.overview_positions: 4610 if args.output is not None: 4611 server.overviewPositionsFile = args.output 4612 4613 server.Overview(show=True, details="positions") 4614 4615 elif args.overview_orders: 4616 if args.output is not None: 4617 server.overviewOrdersFile = args.output 4618 4619 server.Overview(show=True, details="orders") 4620 4621 elif args.overview_analytics: 4622 if args.output is not None: 4623 server.overviewAnalyticsFile = args.output 4624 4625 server.Overview(show=True, details="analytics") 4626 4627 elif args.deals is not None: 4628 if args.output is not None: 4629 server.reportFile = args.output 4630 4631 if 0 <= len(args.deals) < 3: 4632 server.Deals( 4633 start=args.deals[0] if len(args.deals) >= 1 else None, 4634 end=args.deals[1] if len(args.deals) == 2 else None, 4635 show=True, # Always show deals report in console 4636 showCancelled=not args.no_cancelled, # If --no-cancelled key then remove cancelled operations from the deals report. False by default. 4637 ) 4638 4639 else: 4640 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4641 raise Exception("Incorrect value") 4642 4643 elif args.history is not None: 4644 if args.output is not None: 4645 server.historyFile = args.output 4646 4647 if 0 <= len(args.history) < 3: 4648 dataReceived = server.History( 4649 start=args.history[0] if len(args.history) >= 1 else None, 4650 end=args.history[1] if len(args.history) == 2 else None, 4651 interval="hour" if args.interval is None or not args.interval else args.interval, 4652 onlyMissing=False if args.only_missing is None or not args.only_missing else args.only_missing, 4653 csvSep="," if args.csv_sep is None or not args.csv_sep else args.csv_sep, 4654 show=True, # shows all downloaded candles in console 4655 ) 4656 4657 if args.render_chart is not None and dataReceived is not None: 4658 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4659 4660 server.ShowHistoryChart( 4661 candles=dataReceived, 4662 interact=iChart, 4663 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4664 ) 4665 4666 else: 4667 uLogger.error("You must specify 0-2 parameters: [DATE_START] [DATE_END]") 4668 raise Exception("Incorrect value") 4669 4670 elif args.load_history is not None: 4671 histData = server.LoadHistory(filePath=args.load_history) # load data from file and show history in console 4672 4673 if args.render_chart is not None and histData is not None: 4674 iChart = False if args.render_chart.lower() == "ni" or args.render_chart.lower() == "non-interact" else True 4675 server.ticker = os.path.basename(args.load_history) # use filename as ticker name for PriceGenerator's chart 4676 4677 server.ShowHistoryChart( 4678 candles=histData, 4679 interact=iChart, 4680 openInBrowser=False, # False by default, to avoid issues with `permissions denied` to html-file. 4681 ) 4682 4683 elif args.trade is not None: 4684 if 1 <= len(args.trade) <= 5: 4685 server.Trade( 4686 operation=args.trade[0], 4687 lots=int(args.trade[1]) if len(args.trade) >= 2 else 1, 4688 tp=float(args.trade[2]) if len(args.trade) >= 3 else 0., 4689 sl=float(args.trade[3]) if len(args.trade) >= 4 else 0., 4690 expDate=args.trade[4] if len(args.trade) == 5 else "Undefined", 4691 ) 4692 4693 else: 4694 uLogger.error("You must specify 1-5 parameters to open trade: [direction `Buy` or `Sell`] [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4695 4696 elif args.buy is not None: 4697 if 0 <= len(args.buy) <= 4: 4698 server.Buy( 4699 lots=int(args.buy[0]) if len(args.buy) >= 1 else 1, 4700 tp=float(args.buy[1]) if len(args.buy) >= 2 else 0., 4701 sl=float(args.buy[2]) if len(args.buy) >= 3 else 0., 4702 expDate=args.buy[3] if len(args.buy) == 4 else "Undefined", 4703 ) 4704 4705 else: 4706 uLogger.error("You must specify 0-4 parameters to open buy position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4707 4708 elif args.sell is not None: 4709 if 0 <= len(args.sell) <= 4: 4710 server.Sell( 4711 lots=int(args.sell[0]) if len(args.sell) >= 1 else 1, 4712 tp=float(args.sell[1]) if len(args.sell) >= 2 else 0., 4713 sl=float(args.sell[2]) if len(args.sell) >= 3 else 0., 4714 expDate=args.sell[3] if len(args.sell) == 4 else "Undefined", 4715 ) 4716 4717 else: 4718 uLogger.error("You must specify 0-4 parameters to open sell position: [lots, >= 1] [take profit, >= 0] [stop loss, >= 0] [expiration date for TP/SL orders, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4719 4720 elif args.order: 4721 if 4 <= len(args.order) <= 7: 4722 server.Order( 4723 operation=args.order[0], 4724 orderType=args.order[1], 4725 lots=int(args.order[2]), 4726 targetPrice=float(args.order[3]), 4727 limitPrice=float(args.order[4]) if len(args.order) >= 5 else 0., 4728 stopType=args.order[5] if len(args.order) >= 6 else "Limit", 4729 expDate=args.order[6] if len(args.order) == 7 else "Undefined", 4730 ) 4731 4732 else: 4733 uLogger.error("You must specify 4-7 parameters to open order: [direction `Buy` or `Sell`] [order type `Limit` or `Stop`] [lots] [target price] [maybe for stop-order: [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]]. See: `python TKSBrokerAPI.py --help`") 4734 4735 elif args.buy_limit: 4736 server.BuyLimit(lots=int(args.buy_limit[0]), targetPrice=args.buy_limit[1]) 4737 4738 elif args.sell_limit: 4739 server.SellLimit(lots=int(args.sell_limit[0]), targetPrice=args.sell_limit[1]) 4740 4741 elif args.buy_stop: 4742 if 2 <= len(args.buy_stop) <= 7: 4743 server.BuyStop( 4744 lots=int(args.buy_stop[0]), 4745 targetPrice=float(args.buy_stop[1]), 4746 limitPrice=float(args.buy_stop[2]) if len(args.buy_stop) >= 3 else 0., 4747 stopType=args.buy_stop[3] if len(args.buy_stop) >= 4 else "Limit", 4748 expDate=args.buy_stop[4] if len(args.buy_stop) == 5 else "Undefined", 4749 ) 4750 4751 else: 4752 uLogger.error("You must specify 2-5 parameters for buy stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: `python TKSBrokerAPI.py --help`") 4753 4754 elif args.sell_stop: 4755 if 2 <= len(args.sell_stop) <= 7: 4756 server.SellStop( 4757 lots=int(args.sell_stop[0]), 4758 targetPrice=float(args.sell_stop[1]), 4759 limitPrice=float(args.sell_stop[2]) if len(args.sell_stop) >= 3 else 0., 4760 stopType=args.sell_stop[3] if len(args.sell_stop) >= 4 else "Limit", 4761 expDate=args.sell_stop[4] if len(args.sell_stop) == 5 else "Undefined", 4762 ) 4763 4764 else: 4765 uLogger.error("You must specify 2-5 parameters for sell stop-order: [lots] [target price] [limit price, >= 0] [stop type, Limit|SL|TP] [expiration date, Undefined|`%Y-%m-%d %H:%M:%S`]. See: python TKSBrokerAPI.py --help") 4766 4767 # elif args.buy_order_grid is not None: 4768 # # update order grid work with api v2 4769 # if len(args.buy_order_grid) == 2: 4770 # orderParams = server.ParseOrderParameters(operation="Buy", **dict(kw.split('=') for kw in args.buy_order_grid)) 4771 # 4772 # for order in orderParams: 4773 # server.Order(operation="Buy", lots=order["lot"], price=order["price"]) 4774 # 4775 # else: 4776 # uLogger.error("To open grid of pending BUY limit-orders (below current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4777 # 4778 # elif args.sell_order_grid is not None: 4779 # # update order grid work with api v2 4780 # if len(args.sell_order_grid) >= 2: 4781 # orderParams = server.ParseOrderParameters(operation="Sell", **dict(kw.split('=') for kw in args.sell_order_grid)) 4782 # 4783 # for order in orderParams: 4784 # server.Order(operation="Sell", lots=order["lot"], price=order["price"]) 4785 # 4786 # else: 4787 # uLogger.error("To open grid of pending SELL limit-orders (above current price) you must specified 2 parameters: l(ots)=[L_int,...] p(rices)=[P_float,...]. See: `python TKSBrokerAPI.py --help`") 4788 4789 elif args.close_order is not None: 4790 server.CloseOrders(args.close_order) # close only one order 4791 4792 elif args.close_orders is not None: 4793 server.CloseOrders(args.close_orders) # close list of orders 4794 4795 elif args.close_trade: 4796 if not args.ticker: 4797 uLogger.error("`--ticker` key is required for this operation!") 4798 raise Exception("Ticker required") 4799 4800 server.CloseTrades([args.ticker]) # close only one trade 4801 4802 elif args.close_trades is not None: 4803 server.CloseTrades(args.close_trades) # close trades for list of tickers 4804 4805 elif args.close_all is not None: 4806 server.CloseAll(*args.close_all) 4807 4808 elif args.limits: 4809 if args.output is not None: 4810 server.withdrawalLimitsFile = args.output 4811 4812 server.OverviewLimits(show=True) 4813 4814 elif args.user_info: 4815 if args.output is not None: 4816 server.userInfoFile = args.output 4817 4818 server.OverviewUserInfo(show=True) 4819 4820 elif args.account: 4821 if args.output is not None: 4822 server.userAccountsFile = args.output 4823 4824 server.OverviewAccounts(show=True) 4825 4826 else: 4827 uLogger.error("There is no command to execute! One of the possible commands must be selected. See help with `--help` key.") 4828 raise Exception("There is no command to execute") 4829 4830 except Exception: 4831 trace = tb.format_exc() 4832 for e in ["socket.gaierror", "nodename nor servname provided", "or not known", "NewConnectionError", "[Errno 8]", "Failed to establish a new connection"]: 4833 if e in trace: 4834 uLogger.error("Check your Internet connection! Failed to establish connection to broker server!") 4835 break 4836 4837 uLogger.debug(trace) 4838 uLogger.debug("Please, check issues or request a new one at https://github.com/Tim55667757/TKSBrokerAPI/issues") 4839 exitCode = 255 # an error occurred, must be open a ticket for this issue 4840 4841 finally: 4842 finish = datetime.now(tzutc()) 4843 4844 if exitCode == 0: 4845 uLogger.debug("All operations were finished success (summary code is 0).") 4846 4847 else: 4848 uLogger.error("An issue occurred with TKSBrokerAPI module! See full debug log in [{}] or run TKSBrokerAPI once again with the key `--debug-level 10`. Summary code: {}".format( 4849 os.path.abspath(uLog.defaultLogFile), exitCode, 4850 )) 4851 4852 uLogger.debug(">>> TKSBrokerAPI module work duration: [{}]".format(finish - start)) 4853 uLogger.debug(">>> TKSBrokerAPI module finished: [{} UTC], it is [{}] local time".format( 4854 finish.strftime(TKS_PRINT_DATE_TIME_FORMAT), 4855 finish.astimezone(tzlocal()).strftime(TKS_PRINT_DATE_TIME_FORMAT), 4856 )) 4857 4858 if not kwargs: 4859 sys.exit(exitCode) 4860 4861 else: 4862 return exitCode
Main function for work with TKSBrokerAPI in the console.
See examples: